@costrict/csc 4.2.6 → 4.2.7-beta2

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.
@@ -456,10 +456,10 @@ async function appendDeadLetter(entry) {
456
456
  }
457
457
 
458
458
  // src/services/rawDump/worker.ts
459
- import { promises as fs8 } from "fs";
459
+ import { promises as fs9 } from "fs";
460
460
  import { createHash as createHash2 } from "crypto";
461
461
  import os2 from "os";
462
- import path8 from "path";
462
+ import path11 from "path";
463
463
  import { fileURLToPath } from "url";
464
464
 
465
465
  // src/costrict/provider/credentials.ts
@@ -508,8 +508,8 @@ async function saveCoStrictCredentials(credentials) {
508
508
  import { createRequire } from "module";
509
509
  function getVersion() {
510
510
  try {
511
- if (typeof MACRO !== "undefined" && "4.2.6")
512
- return "4.2.6";
511
+ if (typeof MACRO !== "undefined" && "4.2.7-beta2")
512
+ return "4.2.7-beta2";
513
513
  } catch {}
514
514
  try {
515
515
  const require2 = createRequire(import.meta.url);
@@ -594,8 +594,11 @@ async function refreshCoStrictToken(params) {
594
594
 
595
595
  // src/services/rawDump/git.ts
596
596
  import { execFile } from "child_process";
597
+ import { realpathSync } from "fs";
598
+ import path4 from "path";
597
599
  import { promisify } from "util";
598
600
  var execFileAsync = promisify(execFile);
601
+ var TRUNK_BRANCHES = ["origin/main", "origin/master", "main", "master"];
599
602
  async function gitExec(args, cwd) {
600
603
  try {
601
604
  const { stdout } = await execFileAsync("git", args, {
@@ -610,19 +613,63 @@ async function gitExec(args, cwd) {
610
613
  }
611
614
  }
612
615
  async function getRepoInfo(cwd) {
613
- const [repoAddr, repoBranch, gitUserName, gitUserEmail] = await Promise.all([
616
+ const [repoAddr, repoBranch, gitUserName, gitUserEmail, gitRoot] = await Promise.all([
614
617
  gitExec(["remote", "get-url", "origin"], cwd),
615
618
  gitExec(["branch", "--show-current"], cwd),
616
619
  gitExec(["config", "user.name"], cwd),
617
- gitExec(["config", "user.email"], cwd)
620
+ gitExec(["config", "user.email"], cwd),
621
+ gitExec(["rev-parse", "--show-toplevel"], cwd)
618
622
  ]);
619
623
  return {
620
624
  repo_addr: repoAddr,
621
625
  repo_branch: repoBranch,
622
626
  git_user_name: gitUserName,
623
- git_user_email: gitUserEmail
627
+ git_user_email: gitUserEmail,
628
+ git_root: gitRoot
624
629
  };
625
630
  }
631
+ function computeRepoRelativePath(gitRoot, workDir) {
632
+ if (!gitRoot)
633
+ return "";
634
+ const root = tryRealpath(gitRoot);
635
+ const dir = tryRealpath(workDir);
636
+ const rel = path4.relative(root, dir);
637
+ if (rel === "")
638
+ return ".";
639
+ if (rel.startsWith(".."))
640
+ return "";
641
+ return rel;
642
+ }
643
+ function tryRealpath(p) {
644
+ try {
645
+ return realpathSync(p);
646
+ } catch {
647
+ return path4.resolve(p);
648
+ }
649
+ }
650
+ async function getBranchAncestry(cwd) {
651
+ const upstream = await gitExec(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd);
652
+ if (upstream && TRUNK_BRANCHES.includes(upstream)) {
653
+ const forkPoint = await gitExec(["merge-base", upstream, "HEAD"], cwd);
654
+ return { base_branch: upstream, fork_point: forkPoint };
655
+ }
656
+ const candidates = TRUNK_BRANCHES;
657
+ let best = null;
658
+ for (const base of candidates) {
659
+ const forkPoint = await gitExec(["merge-base", base, "HEAD"], cwd);
660
+ if (!forkPoint)
661
+ continue;
662
+ const countOut = await gitExec(["rev-list", "--count", `${forkPoint}..HEAD`], cwd);
663
+ const distance = Number.parseInt(countOut, 10);
664
+ const dist = Number.isNaN(distance) ? Number.MAX_SAFE_INTEGER : distance;
665
+ if (!best || dist < best.distance) {
666
+ best = { base, forkPoint, distance: dist };
667
+ }
668
+ }
669
+ if (best)
670
+ return { base_branch: best.base, fork_point: best.forkPoint };
671
+ return { base_branch: "", fork_point: "" };
672
+ }
626
673
  function countDiffLines(diff) {
627
674
  let count = 0;
628
675
  for (const line of diff.split(`
@@ -706,9 +753,69 @@ function toCommitComment(subject) {
706
753
  return Array.from(subject).slice(0, 150).join("");
707
754
  }
708
755
 
756
+ // src/services/rawDump/activeSessions.ts
757
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, renameSync, realpathSync as realpathSync2 } from "fs";
758
+ import path5 from "path";
759
+ var log2 = createLogger("activeSessions");
760
+ var ACTIVE_SESSIONS_FILE = path5.join(getRawDumpDir(), "csc-active-sessions.json");
761
+ var DEFAULT_WINDOW_MS = 30 * 60 * 1000;
762
+ var FUTURE_TOLERANCE_MS = 5 * 60 * 1000;
763
+ var MAX_ACTIVE_SESSION_IDS = 64;
764
+ function getWindowMs() {
765
+ const v = process.env.CSC_ACTIVE_SESSION_WINDOW_MS;
766
+ if (v) {
767
+ const n = Number(v);
768
+ if (Number.isFinite(n) && n > 0)
769
+ return n;
770
+ }
771
+ return DEFAULT_WINDOW_MS;
772
+ }
773
+ function normalizeCwd(p) {
774
+ if (!p)
775
+ return "";
776
+ let resolved;
777
+ try {
778
+ resolved = realpathSync2(p);
779
+ } catch {
780
+ resolved = path5.resolve(p);
781
+ }
782
+ if (resolved.length > 1 && resolved.endsWith(path5.sep)) {
783
+ resolved = resolved.slice(0, -1);
784
+ }
785
+ return resolved;
786
+ }
787
+ function readMap() {
788
+ try {
789
+ const text = readFileSync2(ACTIVE_SESSIONS_FILE, "utf-8");
790
+ const parsed = JSON.parse(text);
791
+ return parsed && typeof parsed === "object" ? parsed : {};
792
+ } catch {
793
+ return {};
794
+ }
795
+ }
796
+ function getActiveSessionIds(cwd, atMs) {
797
+ const ref = atMs ?? Date.now();
798
+ const windowMs = getWindowMs();
799
+ const refCwd = normalizeCwd(cwd);
800
+ const map = readMap();
801
+ const hits = [];
802
+ for (const [sid, entry] of Object.entries(map)) {
803
+ const delta = ref - entry.lastSeenAt;
804
+ if (delta > windowMs)
805
+ continue;
806
+ if (delta < -FUTURE_TOLERANCE_MS)
807
+ continue;
808
+ if (refCwd && entry.cwd && entry.cwd !== refCwd)
809
+ continue;
810
+ hits.push({ sid, lastSeenAt: entry.lastSeenAt });
811
+ }
812
+ hits.sort((a, b) => b.lastSeenAt - a.lastSeenAt);
813
+ return hits.slice(0, MAX_ACTIVE_SESSION_IDS).map((h) => h.sid);
814
+ }
815
+
709
816
  // src/services/rawDump/localStorage.ts
710
817
  import { promises as fs4 } from "fs";
711
- import path4 from "path";
818
+ import path6 from "path";
712
819
  var DEFAULT_LOCAL_DIR = getRawDumpDir();
713
820
  var RAW_DUMP_MODE = {
714
821
  DISABLED: 0,
@@ -757,12 +864,12 @@ async function writeLocalDump(type, body) {
757
864
  endpoint = "/raw-store/task-summary";
758
865
  } else if (type == "conversation") {
759
866
  ymd = getDateFromTimestamp(getTimestampField("conversation", body));
760
- subdir = path4.join(ymd, body.task_id);
867
+ subdir = path6.join(ymd, body.task_id);
761
868
  fname = body.request_id;
762
869
  endpoint = "/raw-store/task-conversation";
763
870
  } else if (type == "commit") {
764
871
  ymd = getDateFromTimestamp(getTimestampField("commit", body));
765
- subdir = path4.join(normalizeProjectPath2(body.repo_addr), normalizeProjectPath2(body.repo_branch), ymd);
872
+ subdir = path6.join(normalizeProjectPath2(body.repo_addr), normalizeProjectPath2(body.repo_branch), ymd);
766
873
  fname = body.commit_id;
767
874
  endpoint = "/raw-store/commit";
768
875
  } else if (type == "statistics") {
@@ -774,7 +881,7 @@ async function writeLocalDump(type, body) {
774
881
  fname = `${h}-${m}-${s}`;
775
882
  endpoint = "/raw-store/statistics";
776
883
  } else if (type == "raw") {
777
- subdir = path4.join(normalizeProjectPath2(body.project), body.session_id);
884
+ subdir = path6.join(normalizeProjectPath2(body.project), body.session_id);
778
885
  fname = String(body.start_cursor);
779
886
  endpoint = "/raw-store/raw-log";
780
887
  } else {
@@ -782,9 +889,9 @@ async function writeLocalDump(type, body) {
782
889
  fname = "unknown";
783
890
  endpoint = "unknown";
784
891
  }
785
- const dumpDir = path4.join(dir, type, subdir);
892
+ const dumpDir = path6.join(dir, type, subdir);
786
893
  const filename = `${fname}.json`;
787
- const filePath = path4.join(dumpDir, filename);
894
+ const filePath = path6.join(dumpDir, filename);
788
895
  const payload = {
789
896
  _dumpMeta: {
790
897
  type,
@@ -799,10 +906,69 @@ async function writeLocalDump(type, body) {
799
906
  }
800
907
 
801
908
  // src/services/rawDump/queue.ts
909
+ import { promises as fs6 } from "fs";
910
+ import path8 from "path";
911
+
912
+ // src/services/rawDump/lock.ts
802
913
  import { promises as fs5 } from "fs";
803
- import path5 from "path";
804
- var QUEUE_FILE = path5.join(getRawDumpDir(), "csc-work-queue.jsonl");
805
- var QUEUE_LOCK_FILE = path5.join(getRawDumpDir(), "csc-work-queue.lock");
914
+ import path7 from "path";
915
+ var LOCK_INFO_FILE = "lock.json";
916
+ async function isHolderAlive(lockDir) {
917
+ try {
918
+ const text = await fs5.readFile(path7.join(lockDir, LOCK_INFO_FILE), "utf-8");
919
+ const info = JSON.parse(text);
920
+ if (!info.pid || typeof info.pid !== "number")
921
+ return false;
922
+ try {
923
+ process.kill(info.pid, 0);
924
+ return true;
925
+ } catch {
926
+ return false;
927
+ }
928
+ } catch {
929
+ return false;
930
+ }
931
+ }
932
+ async function acquireLock(lockDir, info) {
933
+ let retried = false;
934
+ while (true) {
935
+ try {
936
+ await fs5.mkdir(lockDir, { recursive: false });
937
+ } catch (err) {
938
+ if (err.code === "EEXIST") {
939
+ if (retried)
940
+ return false;
941
+ const alive = await isHolderAlive(lockDir);
942
+ if (alive)
943
+ return false;
944
+ try {
945
+ await fs5.rm(lockDir, { recursive: true, force: true });
946
+ } catch {
947
+ return false;
948
+ }
949
+ retried = true;
950
+ continue;
951
+ }
952
+ return false;
953
+ }
954
+ try {
955
+ await fs5.writeFile(path7.join(lockDir, LOCK_INFO_FILE), JSON.stringify(info), "utf-8");
956
+ return true;
957
+ } catch {
958
+ await releaseLock(lockDir);
959
+ return false;
960
+ }
961
+ }
962
+ }
963
+ async function releaseLock(lockDir) {
964
+ try {
965
+ await fs5.rm(lockDir, { recursive: true, force: true });
966
+ } catch {}
967
+ }
968
+
969
+ // src/services/rawDump/queue.ts
970
+ var QUEUE_FILE = path8.join(getRawDumpDir(), "csc-work-queue.jsonl");
971
+ var QUEUE_LOCK_DIR = path8.join(getRawDumpDir(), "csc-work-queue.lock.d");
806
972
  var MAX_ATTEMPTS = 4;
807
973
  var queue = [];
808
974
  var queueLoaded = false;
@@ -810,7 +976,7 @@ async function loadQueue() {
810
976
  if (queueLoaded)
811
977
  return;
812
978
  try {
813
- const text = await fs5.readFile(QUEUE_FILE, "utf-8");
979
+ const text = await fs6.readFile(QUEUE_FILE, "utf-8");
814
980
  queue = text.split(`
815
981
  `).filter(Boolean).map((line) => {
816
982
  try {
@@ -826,51 +992,37 @@ async function loadQueue() {
826
992
  }
827
993
  function clearQueue() {
828
994
  queue = [];
829
- fs5.writeFile(QUEUE_FILE, "", "utf-8").catch(() => {});
995
+ fs6.writeFile(QUEUE_FILE, "", "utf-8").catch(() => {});
830
996
  }
831
997
  function getQueue() {
832
998
  return [...queue];
833
999
  }
834
1000
  async function acquireQueueLock() {
835
- try {
836
- try {
837
- const stat2 = await fs5.readFile(QUEUE_LOCK_FILE, "utf-8");
838
- const pid = parseInt(stat2, 10);
839
- if (!isNaN(pid) && pid !== process.pid) {
840
- try {
841
- process.kill(pid, 0);
842
- return false;
843
- } catch {}
844
- }
845
- } catch {}
846
- await fs5.writeFile(QUEUE_LOCK_FILE, String(process.pid), "utf-8");
847
- return true;
848
- } catch {
849
- return false;
850
- }
1001
+ return acquireLock(QUEUE_LOCK_DIR, {
1002
+ pid: process.pid,
1003
+ startedAt: new Date().toISOString()
1004
+ });
851
1005
  }
852
1006
  async function releaseQueueLock() {
853
- try {
854
- await fs5.writeFile(QUEUE_LOCK_FILE, "", "utf-8");
855
- } catch {}
1007
+ await releaseLock(QUEUE_LOCK_DIR);
856
1008
  }
857
1009
 
858
1010
  // src/services/rawDump/history.ts
859
- import { promises as fs6 } from "fs";
860
- import path6 from "path";
861
- var HISTORY_FILE = path6.join(getCscDir(), "history.jsonl");
1011
+ import { promises as fs7 } from "fs";
1012
+ import path9 from "path";
1013
+ var HISTORY_FILE = path9.join(getCscDir(), "history.jsonl");
862
1014
  var cache = null;
863
1015
  async function loadHistory() {
864
1016
  const items = [];
865
1017
  let fileStat = null;
866
1018
  try {
867
- fileStat = await fs6.stat(HISTORY_FILE);
1019
+ fileStat = await fs7.stat(HISTORY_FILE);
868
1020
  } catch {
869
1021
  cache = null;
870
1022
  return [];
871
1023
  }
872
1024
  try {
873
- const content = await fs6.readFile(HISTORY_FILE, "utf-8");
1025
+ const content = await fs7.readFile(HISTORY_FILE, "utf-8");
874
1026
  const lines = content.split(`
875
1027
  `);
876
1028
  for (const line of lines) {
@@ -894,7 +1046,7 @@ async function loadHistory() {
894
1046
  async function autoLoadHistory() {
895
1047
  let fileStat = null;
896
1048
  try {
897
- fileStat = await fs6.stat(HISTORY_FILE);
1049
+ fileStat = await fs7.stat(HISTORY_FILE);
898
1050
  } catch {
899
1051
  if (cache) {
900
1052
  return null;
@@ -910,7 +1062,7 @@ async function getProjectDirs() {
910
1062
  if (cache) {
911
1063
  let fileStat = null;
912
1064
  try {
913
- fileStat = await fs6.stat(HISTORY_FILE);
1065
+ fileStat = await fs7.stat(HISTORY_FILE);
914
1066
  } catch {
915
1067
  return [...new Set(cache.items.map((item) => item.project))];
916
1068
  }
@@ -923,8 +1075,8 @@ async function getProjectDirs() {
923
1075
  }
924
1076
 
925
1077
  // src/services/rawDump/session.ts
926
- import { promises as fs7 } from "fs";
927
- import path7 from "path";
1078
+ import { promises as fs8 } from "fs";
1079
+ import path10 from "path";
928
1080
 
929
1081
  // src/services/rawDump/parse.ts
930
1082
  function buildFlows(events) {
@@ -983,12 +1135,15 @@ function createConversation(convEvents) {
983
1135
  duration: 0,
984
1136
  sender: "",
985
1137
  diffs: [],
1138
+ toolEvents: [],
1139
+ model: "",
1140
+ promptMode: "",
986
1141
  preview: "",
987
1142
  requestContent: "",
988
1143
  responseContent: ""
989
1144
  };
990
1145
  }
991
- function fillConversation(conv) {
1146
+ function fillConversation(conv, allSessionEvents) {
992
1147
  if (conv.diffs.length > 0 || conv.startTime || conv.inputTokens > 0)
993
1148
  return;
994
1149
  let inputTokens = 0;
@@ -1028,12 +1183,19 @@ function fillConversation(conv) {
1028
1183
  responseTexts.push(text);
1029
1184
  }
1030
1185
  const diffs = [];
1186
+ const toolEvents = [];
1187
+ let model = "";
1031
1188
  conv.events.forEach((evt) => {
1032
1189
  if (evt.type === "assistant") {
1033
- diffs.push(...extractEventToolDiff(evt, conv.events));
1190
+ diffs.push(...extractEventToolDiff(evt, allSessionEvents));
1191
+ toolEvents.push(...extractEventToolEvents(evt, allSessionEvents));
1192
+ if (!model && evt.message?.model)
1193
+ model = evt.message.model;
1034
1194
  }
1035
1195
  });
1036
1196
  conv.diffs = diffs;
1197
+ conv.toolEvents = toolEvents;
1198
+ conv.model = model;
1037
1199
  conv.events.forEach((evt) => {
1038
1200
  if (evt.type === "assistant") {
1039
1201
  const { error_code, error_reason } = extractError(evt);
@@ -1246,6 +1408,118 @@ function extractEventToolDiff(assistantEvent, allEvents) {
1246
1408
  }
1247
1409
  return diffs;
1248
1410
  }
1411
+ var FILE_EDIT_TOOLS = new Set(["Edit", "MultiEdit", "Write", "NotebookEdit"]);
1412
+ var COMMAND_TOOLS = new Set(["Bash", "BashOutput"]);
1413
+ var MAX_TOOL_CONTENT = 32 * 1024;
1414
+ function truncateToolContent(s) {
1415
+ if (s === undefined)
1416
+ return { value: undefined, truncated: false };
1417
+ if (s.length <= MAX_TOOL_CONTENT)
1418
+ return { value: s, truncated: false };
1419
+ return { value: s.slice(0, MAX_TOOL_CONTENT), truncated: true };
1420
+ }
1421
+ function classifyToolKind(name) {
1422
+ if (name === "Edit" || name === "MultiEdit")
1423
+ return "edit";
1424
+ if (name === "Write")
1425
+ return "write";
1426
+ if (name === "NotebookEdit")
1427
+ return "notebook";
1428
+ if (COMMAND_TOOLS.has(name))
1429
+ return "command";
1430
+ if (name === "Read" || name === "NotebookRead")
1431
+ return "read";
1432
+ if (name === "Grep" || name === "Glob" || name === "WebSearch")
1433
+ return "search";
1434
+ return "other";
1435
+ }
1436
+ function inferExitCode(tur) {
1437
+ if (!tur)
1438
+ return;
1439
+ if (tur.interrupted === true)
1440
+ return 1;
1441
+ return 0;
1442
+ }
1443
+ function extractEventToolEvents(assistantEvent, allEvents) {
1444
+ const events = [];
1445
+ const msgUuid = assistantEvent.uuid;
1446
+ if (!msgUuid)
1447
+ return events;
1448
+ const content = assistantEvent.message?.content;
1449
+ if (!Array.isArray(content))
1450
+ return events;
1451
+ const resultById = new Map;
1452
+ for (const m of allEvents) {
1453
+ if (m.parentUuid !== msgUuid || m.type !== "user")
1454
+ continue;
1455
+ const tur = m.toolUseResult ?? m.tool_use_result;
1456
+ if (!tur)
1457
+ continue;
1458
+ if (Array.isArray(m.message?.content)) {
1459
+ for (const b of m.message.content) {
1460
+ if (b?.type === "tool_result" && typeof b.tool_use_id === "string") {
1461
+ resultById.set(b.tool_use_id, tur);
1462
+ }
1463
+ }
1464
+ }
1465
+ }
1466
+ for (const block of content) {
1467
+ if (block?.type !== "tool_use")
1468
+ continue;
1469
+ const toolName = block.name ?? "";
1470
+ if (!toolName)
1471
+ continue;
1472
+ const toolUseId = typeof block.id === "string" ? block.id : undefined;
1473
+ const input = block.input ?? {};
1474
+ const tur = toolUseId ? resultById.get(toolUseId) : undefined;
1475
+ const filePath = input.file_path ?? input.notebook_path ?? tur?.filePath ?? tur?.file_path ?? "";
1476
+ const isFileEdit = FILE_EDIT_TOOLS.has(toolName);
1477
+ const isCommand = COMMAND_TOOLS.has(toolName);
1478
+ let before;
1479
+ let after;
1480
+ if (isFileEdit) {
1481
+ const edits = input.edits;
1482
+ if (Array.isArray(edits)) {
1483
+ const befores = [];
1484
+ const afters = [];
1485
+ for (const e of edits) {
1486
+ if (typeof e?.old_string === "string")
1487
+ befores.push(e.old_string);
1488
+ if (typeof e?.new_string === "string")
1489
+ afters.push(e.new_string);
1490
+ }
1491
+ if (befores.length)
1492
+ before = befores.join(`
1493
+ `);
1494
+ if (afters.length)
1495
+ after = afters.join(`
1496
+ `);
1497
+ }
1498
+ if (before === undefined) {
1499
+ before = input.old_string ?? tur?.oldString ?? tur?.old_string ?? tur?.originalFile ?? tur?.original_file;
1500
+ }
1501
+ if (after === undefined) {
1502
+ after = input.new_string ?? input.new_source ?? input.content ?? tur?.newString ?? tur?.new_string ?? tur?.content ?? tur?.updated_file;
1503
+ }
1504
+ }
1505
+ const beforeCapped = truncateToolContent(before);
1506
+ const afterCapped = truncateToolContent(after);
1507
+ const touchedFiles = filePath ? [filePath] : [];
1508
+ events.push({
1509
+ name: toolName,
1510
+ source: isFileEdit ? "ai" : toolName,
1511
+ event_kind: classifyToolKind(toolName),
1512
+ tool_use_id: toolUseId,
1513
+ touched_files: touchedFiles,
1514
+ command: isCommand ? input.command : undefined,
1515
+ exit_code: isCommand ? inferExitCode(tur) : undefined,
1516
+ before: beforeCapped.value,
1517
+ after: afterCapped.value,
1518
+ truncated: beforeCapped.truncated || afterCapped.truncated || undefined
1519
+ });
1520
+ }
1521
+ return events;
1522
+ }
1249
1523
  function parseSession(cacheConversations, events) {
1250
1524
  const sessionId = events.find((e) => e.sessionId)?.sessionId || "unknown";
1251
1525
  const version = events.find((e) => e.version)?.version || "";
@@ -1265,6 +1539,23 @@ function parseSession(cacheConversations, events) {
1265
1539
  totalCacheCreationInputTokens += evt.message.usage.cache_creation_input_tokens || 0;
1266
1540
  }
1267
1541
  });
1542
+ const idxByEvent = new Map;
1543
+ events.forEach((e, i) => idxByEvent.set(e, i));
1544
+ const permissionMarks = events.map((e, idx) => typeof e.permissionMode === "string" && e.permissionMode ? { idx, mode: e.permissionMode } : null).filter((x) => x !== null);
1545
+ const lastSessionMode = permissionMarks.length > 0 ? permissionMarks[permissionMarks.length - 1].mode : "";
1546
+ const resolvePromptMode = (conv) => {
1547
+ if (permissionMarks.length === 0)
1548
+ return "";
1549
+ const convIdx = idxByEvent.get(conv.userEvent) ?? -1;
1550
+ let mode = "";
1551
+ for (const p of permissionMarks) {
1552
+ if (p.idx <= convIdx)
1553
+ mode = p.mode;
1554
+ else
1555
+ break;
1556
+ }
1557
+ return mode || lastSessionMode;
1558
+ };
1268
1559
  const flows = buildFlows(events);
1269
1560
  flows.forEach((flow) => {
1270
1561
  const convResults = splitFlowToConversations(flow);
@@ -1282,7 +1573,10 @@ function parseSession(cacheConversations, events) {
1282
1573
  conversations.forEach((conv, i) => {
1283
1574
  conv.index = i;
1284
1575
  });
1285
- conversations.forEach((conv) => fillConversation(conv));
1576
+ conversations.forEach((conv) => fillConversation(conv, events));
1577
+ conversations.forEach((conv) => {
1578
+ conv.promptMode = resolvePromptMode(conv);
1579
+ });
1286
1580
  return {
1287
1581
  id: sessionId,
1288
1582
  version,
@@ -1300,10 +1594,10 @@ function parseSession(cacheConversations, events) {
1300
1594
  }
1301
1595
 
1302
1596
  // src/services/rawDump/session.ts
1303
- var log2 = createLogger("session");
1597
+ var log3 = createLogger("session");
1304
1598
  async function loadSessionMessages(sessionFile) {
1305
1599
  try {
1306
- const text = await fs7.readFile(sessionFile, "utf-8");
1600
+ const text = await fs8.readFile(sessionFile, "utf-8");
1307
1601
  const messages = text.split(`
1308
1602
  `).filter(Boolean).map((line) => {
1309
1603
  try {
@@ -1312,7 +1606,7 @@ async function loadSessionMessages(sessionFile) {
1312
1606
  return null;
1313
1607
  }
1314
1608
  }).filter((m) => m !== null);
1315
- log2.debug("loaded messages from file", {
1609
+ log3.debug("loaded messages from file", {
1316
1610
  sessionFile,
1317
1611
  count: messages.length
1318
1612
  });
@@ -1335,10 +1629,10 @@ async function getParsedSession(taskDir, sessionId) {
1335
1629
  const sessionDir = getSessionDirectory(taskDir);
1336
1630
  const cacheKey = `${sessionDir}:${sessionId}`;
1337
1631
  const cached = sessionMessagesCache.get(cacheKey);
1338
- const sessionFile = path7.join(sessionDir, `${sessionId}.jsonl`);
1632
+ const sessionFile = path10.join(sessionDir, `${sessionId}.jsonl`);
1339
1633
  let stats;
1340
1634
  try {
1341
- stats = await fs7.stat(sessionFile);
1635
+ stats = await fs8.stat(sessionFile);
1342
1636
  } catch {
1343
1637
  return {
1344
1638
  sessionJsonlFileName: sessionFile,
@@ -1351,7 +1645,7 @@ async function getParsedSession(taskDir, sessionId) {
1351
1645
  }
1352
1646
  const needsReanalysis = !cached || cached.fileTimestamp !== stats.mtimeMs || cached.fileSize !== stats.size || !cached.parsedSession;
1353
1647
  if (!needsReanalysis && cached) {
1354
- log2.debug("using cached parsed session", { sessionId });
1648
+ log3.debug("using cached parsed session", { sessionId });
1355
1649
  return cached;
1356
1650
  }
1357
1651
  cleanupOldSessions();
@@ -1359,7 +1653,7 @@ async function getParsedSession(taskDir, sessionId) {
1359
1653
  const messages = cached?.messages ?? await loadSessionMessages(sessionFile);
1360
1654
  const elapsed = Date.now() - start;
1361
1655
  if (elapsed > 100) {
1362
- log2.info("loadSessionMessages slow", { sessionId, elapsedMs: elapsed });
1656
+ log3.info("loadSessionMessages slow", { sessionId, elapsedMs: elapsed });
1363
1657
  }
1364
1658
  const cachedConvs = cached?.parsedSession?.conversations ?? [];
1365
1659
  const parsedSession = parseSession(cachedConvs, messages);
@@ -1376,7 +1670,7 @@ async function getParsedSession(taskDir, sessionId) {
1376
1670
  }
1377
1671
 
1378
1672
  // src/services/rawDump/statistics.ts
1379
- var log3 = createLogger("worker");
1673
+ var log4 = createLogger("worker");
1380
1674
  async function updateStatisticsForUpload() {
1381
1675
  const dailyStats = await statHistorySessions();
1382
1676
  for (const [_dateKey, stats] of dailyStats) {
@@ -1390,7 +1684,7 @@ async function updateStatisticsForUpload() {
1390
1684
  } else {
1391
1685
  const startTime = new Date(_dateKey);
1392
1686
  if (state_statistics.clean_time && startTime < new Date(state_statistics.clean_time)) {
1393
- log3.info("statistics skipped: too old", {
1687
+ log4.info("statistics skipped: too old", {
1394
1688
  startTime: startTime.toISOString(),
1395
1689
  clean_time: state_statistics.clean_time
1396
1690
  });
@@ -1500,9 +1794,21 @@ async function statHistorySessions() {
1500
1794
  }
1501
1795
 
1502
1796
  // src/services/rawDump/worker.ts
1503
- var log4 = createLogger("worker");
1797
+ var log5 = createLogger("worker");
1504
1798
  var CSC_DIR = getCscDir();
1505
1799
  var REQUEST_TIMEOUT_MS = 30000;
1800
+ var TOOL_EVENTS_PAYLOAD_BUDGET = 256 * 1024;
1801
+ function capToolEventsPayload(events) {
1802
+ let used = 0;
1803
+ return events.map((e) => {
1804
+ const size = (e.before?.length ?? 0) + (e.after?.length ?? 0);
1805
+ if (size > 0 && used + size > TOOL_EVENTS_PAYLOAD_BUDGET) {
1806
+ return { ...e, before: undefined, after: undefined, truncated: true };
1807
+ }
1808
+ used += size;
1809
+ return e;
1810
+ });
1811
+ }
1506
1812
  var repoInfoCache = new Map;
1507
1813
  var REPO_CACHE_TTL_MS = 60000;
1508
1814
  async function getCachedRepoInfo(directory) {
@@ -1515,13 +1821,13 @@ async function getCachedRepoInfo(directory) {
1515
1821
  return repoInfo;
1516
1822
  }
1517
1823
  async function processTask(task, authData) {
1518
- log4.info("processing task:", { task });
1824
+ log5.info("processing task:", { task });
1519
1825
  await uploadRaw(task.sessionId, task.directory, authData);
1520
1826
  const repoInfo = await getCachedRepoInfo(task.directory);
1521
1827
  const cacheEntry = await getParsedSession(task.directory, task.sessionId);
1522
1828
  const parsedSession = cacheEntry.parsedSession;
1523
1829
  if (parsedSession.conversations.length === 0) {
1524
- log4.warn("no conversations found", { task });
1830
+ log5.warn("no conversations found", { task });
1525
1831
  }
1526
1832
  await uploadSummary({ sessionId: task.sessionId, directory: task.directory, parsedSession }, authData);
1527
1833
  for (const conv of parsedSession.conversations) {
@@ -1531,7 +1837,7 @@ async function processTask(task, authData) {
1531
1837
  conversation: conv
1532
1838
  }, authData, { repoInfo });
1533
1839
  }
1534
- log4.info("task completed", {
1840
+ log5.info("task completed", {
1535
1841
  sessionId: task.sessionId,
1536
1842
  conversationCount: parsedSession.conversations.length
1537
1843
  });
@@ -1568,12 +1874,12 @@ function getRawDumpUrl(baseUrl, endpoint, isAnonymous = false) {
1568
1874
  async function uploadReport(authData, endpoint, body) {
1569
1875
  const mode = getRawDumpMode();
1570
1876
  if (mode === RAW_DUMP_MODE.DISABLED) {
1571
- log4.debug(`dump disabled, skipping ${endpoint}`);
1877
+ log5.debug(`dump disabled, skipping ${endpoint}`);
1572
1878
  return;
1573
1879
  }
1574
1880
  const type = endpoint === "/raw-store/task-conversation" ? "conversation" : endpoint === "/raw-store/task-summary" ? "summary" : endpoint === "/raw-store/commit" ? "commit" : endpoint === "/raw-store/statistics" ? "statistics" : endpoint === "/raw-store/raw-log" ? "raw" : "unknown";
1575
1881
  if (type === "unknown") {
1576
- log4.warn("unknown endpoint, skipping local dump", { endpoint });
1882
+ log5.warn("unknown endpoint, skipping local dump", { endpoint });
1577
1883
  } else if (mode === RAW_DUMP_MODE.LOCAL || mode === RAW_DUMP_MODE.BOTH) {
1578
1884
  await writeLocalDump(type, body);
1579
1885
  }
@@ -1584,10 +1890,10 @@ async function uploadReport(authData, endpoint, body) {
1584
1890
  async function writeRemoteDump(endpoint, authData, body) {
1585
1891
  const isAnonymous = authData.isAnonymous ?? false;
1586
1892
  const url = getRawDumpUrl(authData.baseUrl, endpoint, isAnonymous);
1587
- log4.debug(`POST ${endpoint}`, { url, authData, isAnonymous });
1893
+ log5.debug(`POST ${endpoint}`, { url, authData, isAnonymous });
1588
1894
  try {
1589
1895
  await postJson(url, body, authData.headers);
1590
- log4.debug(`POST ${endpoint} ok`);
1896
+ log5.debug(`POST ${endpoint} ok`);
1591
1897
  return;
1592
1898
  } catch (err) {
1593
1899
  throw err instanceof UploadError ? err : new Error(`${endpoint} failed after 3 attempts`);
@@ -1628,18 +1934,18 @@ async function postJson(url, body, headers, maxAttempts = 3) {
1628
1934
  const text = await res.text().catch(() => "");
1629
1935
  lastError = new Error(`${url} failed: ${res.status} ${text}`);
1630
1936
  if (res.status === 429 || res.status >= 500) {
1631
- log4.warn(`retryable error ${res.status}, will retry`, { url, attempt, status: res.status });
1937
+ log5.warn(`retryable error ${res.status}, will retry`, { url, attempt, status: res.status });
1632
1938
  continue;
1633
1939
  }
1634
1940
  if (res.status === 401 || res.status === 502 || res.status === 503) {
1635
- log4.warn(`retryable status ${res.status}, will retry`, { url, attempt });
1941
+ log5.warn(`retryable status ${res.status}, will retry`, { url, attempt });
1636
1942
  continue;
1637
1943
  }
1638
1944
  throw lastError;
1639
1945
  } catch (err) {
1640
1946
  lastError = err instanceof Error ? err : new Error(String(err));
1641
1947
  const isAbort = lastError.name === "AbortError";
1642
- log4.warn(`${isAbort ? "timeout" : "network error"}, will retry`, {
1948
+ log5.warn(`${isAbort ? "timeout" : "network error"}, will retry`, {
1643
1949
  url,
1644
1950
  attempt,
1645
1951
  timeoutMs: REQUEST_TIMEOUT_MS,
@@ -1649,7 +1955,7 @@ async function postJson(url, body, headers, maxAttempts = 3) {
1649
1955
  clearTimeout(timer);
1650
1956
  }
1651
1957
  }
1652
- log4.error("postJson failed", {
1958
+ log5.error("postJson failed", {
1653
1959
  url,
1654
1960
  headers: headersObj,
1655
1961
  error: lastError?.message
@@ -1679,15 +1985,15 @@ function detectOs() {
1679
1985
  async function auth() {
1680
1986
  let creds = await loadCoStrictCredentials();
1681
1987
  if (!creds?.access_token) {
1682
- log4.debug("auth: not authenticated");
1988
+ log5.debug("auth: not authenticated");
1683
1989
  throw new Error("Not authenticated");
1684
1990
  }
1685
- log4.debug("credentials loaded", {
1991
+ log5.debug("credentials loaded", {
1686
1992
  hasRefreshToken: !!creds.refresh_token,
1687
1993
  baseUrl: creds.base_url
1688
1994
  });
1689
1995
  if (creds.refresh_token && !isCoStrictTokenValid(creds)) {
1690
- log4.debug("token expired, refreshing...");
1996
+ log5.debug("token expired, refreshing...");
1691
1997
  const next = await refreshCoStrictToken({
1692
1998
  baseUrl: creds.base_url || process.env.COSTRICT_BASE_URL || process.env.COSTRICT_RAW_DUMP_BASE_URL || "https://zgsm.sangfor.com",
1693
1999
  refreshToken: creds.refresh_token,
@@ -1706,7 +2012,7 @@ async function auth() {
1706
2012
  access_token: next.access_token,
1707
2013
  refresh_token: next.refresh_token
1708
2014
  };
1709
- log4.debug("token refreshed");
2015
+ log5.debug("token refreshed");
1710
2016
  }
1711
2017
  const headers = new Headers;
1712
2018
  headers.set("Authorization", `Bearer ${creds.access_token}`);
@@ -1715,8 +2021,8 @@ async function auth() {
1715
2021
  headers.set("X-Title", "CoStrict-CLI");
1716
2022
  let version = "unknown";
1717
2023
  try {
1718
- const pkgPath = path8.resolve(fileURLToPath(import.meta.url), "../../package.json");
1719
- const pkg = JSON.parse(await fs8.readFile(pkgPath, "utf-8"));
2024
+ const pkgPath = path11.resolve(fileURLToPath(import.meta.url), "../../package.json");
2025
+ const pkg = JSON.parse(await fs9.readFile(pkgPath, "utf-8"));
1720
2026
  version = pkg.version ?? "unknown";
1721
2027
  } catch {}
1722
2028
  headers.set("X-Costrict-Version", `csc-${version}`);
@@ -1735,7 +2041,7 @@ async function auth() {
1735
2041
  }
1736
2042
  const user = parseUser(accessPayload, refreshPayload);
1737
2043
  const baseUrl = resolveRawDumpBaseUrl(creds.base_url);
1738
- log4.debug("auth success", {
2044
+ log5.debug("auth success", {
1739
2045
  baseUrl,
1740
2046
  user_id: user.user_id,
1741
2047
  clientId,
@@ -1756,20 +2062,20 @@ async function authWithFallback() {
1756
2062
  } catch (err) {
1757
2063
  let version = "unknown";
1758
2064
  try {
1759
- const pkgPath = path8.resolve(fileURLToPath(import.meta.url), "../../package.json");
1760
- const pkg = JSON.parse(await fs8.readFile(pkgPath, "utf-8"));
2065
+ const pkgPath = path11.resolve(fileURLToPath(import.meta.url), "../../package.json");
2066
+ const pkg = JSON.parse(await fs9.readFile(pkgPath, "utf-8"));
1761
2067
  version = pkg.version ?? "unknown";
1762
2068
  } catch {}
1763
2069
  let deviceId = process.env.CSC_DEVICE_ID;
1764
2070
  if (!deviceId) {
1765
- const deviceIdFile = path8.join(getLocalDumpDir(), "device-id");
2071
+ const deviceIdFile = path11.join(getLocalDumpDir(), "device-id");
1766
2072
  try {
1767
- deviceId = (await fs8.readFile(deviceIdFile, "utf-8")).trim();
2073
+ deviceId = (await fs9.readFile(deviceIdFile, "utf-8")).trim();
1768
2074
  } catch {}
1769
2075
  if (!deviceId) {
1770
2076
  deviceId = generateMachineId();
1771
2077
  try {
1772
- await fs8.writeFile(deviceIdFile, deviceId, "utf-8");
2078
+ await fs9.writeFile(deviceIdFile, deviceId, "utf-8");
1773
2079
  } catch {}
1774
2080
  }
1775
2081
  process.env.CSC_DEVICE_ID = deviceId;
@@ -1781,7 +2087,7 @@ async function authWithFallback() {
1781
2087
  headers.set("zgsm-client-ide", "cli");
1782
2088
  headers.set("X-Costrict-Version", `csc-${version}`);
1783
2089
  headers.set("User-Agent", `csc/${version}`);
1784
- log4.info("auth failed, falling back to anonymous interface", {
2090
+ log5.info("auth failed, falling back to anonymous interface", {
1785
2091
  deviceId,
1786
2092
  version,
1787
2093
  error: err instanceof Error ? err.message : String(err)
@@ -1802,20 +2108,20 @@ async function authWithFallback() {
1802
2108
  async function uploadRaw(sessionId, directory, authData) {
1803
2109
  const normalizedProject = normalizeProjectPath2(directory);
1804
2110
  const stateKey = `${normalizedProject}/${sessionId}`;
1805
- const filePath = path8.join(CSC_DIR, "projects", normalizedProject, `${sessionId}.jsonl`);
2111
+ const filePath = path11.join(CSC_DIR, "projects", normalizedProject, `${sessionId}.jsonl`);
1806
2112
  const lastReportedCount = state_raw.logs[stateKey] ?? 0;
1807
2113
  let fileContent;
1808
2114
  try {
1809
- fileContent = await fs8.readFile(filePath, "utf-8");
2115
+ fileContent = await fs9.readFile(filePath, "utf-8");
1810
2116
  } catch (err) {
1811
- log4.warn("raw file not found, skip", { filePath });
2117
+ log5.warn("raw file not found, skip", { filePath });
1812
2118
  return;
1813
2119
  }
1814
2120
  const lines = fileContent.split(`
1815
2121
  `).filter((line) => line.trim());
1816
2122
  const newLines = lines.slice(lastReportedCount);
1817
2123
  if (newLines.length === 0) {
1818
- log4.debug("raw no new data", { stateKey, lastReportedCount, total: lines.length });
2124
+ log5.debug("raw no new data", { stateKey, lastReportedCount, total: lines.length });
1819
2125
  return;
1820
2126
  }
1821
2127
  const events = [];
@@ -1826,7 +2132,7 @@ async function uploadRaw(sessionId, directory, authData) {
1826
2132
  } catch {}
1827
2133
  }
1828
2134
  if (events.length === 0) {
1829
- log4.warn("raw no valid events", { stateKey });
2135
+ log5.warn("raw no valid events", { stateKey });
1830
2136
  return;
1831
2137
  }
1832
2138
  const BATCH_SIZE = 50;
@@ -1843,19 +2149,19 @@ async function uploadRaw(sessionId, directory, authData) {
1843
2149
  };
1844
2150
  await uploadReport(authData, "/raw-store/raw-log", body);
1845
2151
  offset += BATCH_SIZE;
1846
- log4.debug("raw batch uploaded", { stateKey, batchOffset: offset, batchSize: batch.length });
2152
+ log5.debug("raw batch uploaded", { stateKey, batchOffset: offset, batchSize: batch.length });
1847
2153
  }
1848
2154
  state_raw.logs[stateKey] = lastReportedCount + newLines.length;
1849
2155
  state_raw.total = Object.keys(state_raw.logs).length;
1850
2156
  await saveRaw();
1851
- log4.info("raw uploaded", { stateKey, count: events.length });
2157
+ log5.info("raw uploaded", { stateKey, count: events.length });
1852
2158
  }
1853
2159
  async function uploadConversation(payload, authData, options) {
1854
2160
  const conv = payload.conversation;
1855
2161
  const requestID = conv.id;
1856
2162
  const key = `${payload.sessionId}:${requestID}`;
1857
2163
  if (state_conv.conversation[key]) {
1858
- log4.info("conversation skipped: already uploaded", {
2164
+ log5.info("conversation skipped: already uploaded", {
1859
2165
  task_id: payload.sessionId,
1860
2166
  request_id: requestID,
1861
2167
  last_reported: state_conv.conversation[key]
@@ -1863,7 +2169,7 @@ async function uploadConversation(payload, authData, options) {
1863
2169
  return false;
1864
2170
  }
1865
2171
  if (state_conv.clean_time && payload.conversation.startTime && payload.conversation.startTime.getTime() < new Date(state_conv.clean_time).getTime()) {
1866
- log4.info("conversation skipped: too old", {
2172
+ log5.info("conversation skipped: too old", {
1867
2173
  task_id: payload.sessionId,
1868
2174
  request_id: requestID,
1869
2175
  clean_time: state_conv.clean_time,
@@ -1876,18 +2182,21 @@ async function uploadConversation(payload, authData, options) {
1876
2182
  const diffLines = rawDiff ? countDiffLines(rawDiff) : 0;
1877
2183
  const files = conv.diffs.map((d) => d.file);
1878
2184
  const repoInfo = options?.repoInfo ?? await getRepoInfo(payload.directory);
2185
+ const gitRoot = repoInfo.git_root ?? "";
1879
2186
  const body = {
1880
2187
  task_id: payload.sessionId,
1881
2188
  request_id: requestID,
1882
- prompt_mode: "",
2189
+ prompt_mode: conv.promptMode,
1883
2190
  mode: "code",
1884
- model: "",
2191
+ model: conv.model,
1885
2192
  start_time: formatIso(conv.startTime),
1886
2193
  end_time: formatIso(conv.endTime),
1887
2194
  process_time: conv.duration,
1888
2195
  process_ttft: 0,
1889
- upstream_tokens: conv.inputTokens,
2196
+ upstream_tokens: conv.inputTokens + conv.cacheReadInputTokens + conv.cacheCreationInputTokens,
1890
2197
  downstream_tokens: conv.outputTokens,
2198
+ cache_read_tokens: conv.cacheReadInputTokens,
2199
+ cache_creation_tokens: conv.cacheCreationInputTokens,
1891
2200
  cost: 0,
1892
2201
  sender: conv.sender,
1893
2202
  request_content: conv.requestContent,
@@ -1898,11 +2207,14 @@ async function uploadConversation(payload, authData, options) {
1898
2207
  diff: rawDiff,
1899
2208
  diff_lines: diffLines,
1900
2209
  files,
2210
+ tool_events: capToolEventsPayload(conv.toolEvents),
1901
2211
  repo_addr: repoInfo.repo_addr,
1902
2212
  repo_branch: repoInfo.repo_branch,
1903
- work_dir: payload.directory
2213
+ work_dir: payload.directory,
2214
+ git_root: gitRoot,
2215
+ repo_relative_path: computeRepoRelativePath(gitRoot, payload.directory)
1904
2216
  };
1905
- log4.debug("conversation uploading", {
2217
+ log5.debug("conversation uploading", {
1906
2218
  task_id: payload.sessionId,
1907
2219
  request_id: requestID
1908
2220
  });
@@ -1913,7 +2225,7 @@ async function uploadConversation(payload, authData, options) {
1913
2225
  if (wasIncomplete)
1914
2226
  state_conv.incomplete--;
1915
2227
  await saveConversation();
1916
- log4.info("conversation uploaded", {
2228
+ log5.info("conversation uploaded", {
1917
2229
  task_id: payload.sessionId,
1918
2230
  request_id: requestID,
1919
2231
  upstream_tokens: body.upstream_tokens,
@@ -1923,12 +2235,12 @@ async function uploadConversation(payload, authData, options) {
1923
2235
  }
1924
2236
  async function uploadSummary(payload, authData) {
1925
2237
  const parsed = payload.parsedSession;
1926
- log4.debug("uploadSummary start", {
2238
+ log5.debug("uploadSummary start", {
1927
2239
  sessionId: payload.sessionId,
1928
2240
  conversationCount: parsed.conversations.length
1929
2241
  });
1930
2242
  if (state_summary.summary[payload.sessionId]) {
1931
- log4.info("summary skipped: already uploaded", {
2243
+ log5.info("summary skipped: already uploaded", {
1932
2244
  task_id: payload.sessionId
1933
2245
  });
1934
2246
  return;
@@ -1936,7 +2248,7 @@ async function uploadSummary(payload, authData) {
1936
2248
  const firstConv = parsed.conversations[0];
1937
2249
  const startTime = firstConv?.userEvent.timestamp ? new Date(firstConv.userEvent.timestamp) : new Date(Date.now());
1938
2250
  if (state_summary.clean_time && startTime < new Date(state_summary.clean_time)) {
1939
- log4.info("summary skipped: too old", {
2251
+ log5.info("summary skipped: too old", {
1940
2252
  startTime: startTime.toISOString(),
1941
2253
  clean_time: state_summary.clean_time
1942
2254
  });
@@ -1960,12 +2272,12 @@ async function uploadSummary(payload, authData) {
1960
2272
  if (wasIncomplete)
1961
2273
  state_summary.incomplete--;
1962
2274
  await saveSummary();
1963
- log4.info("summary uploaded", { task_id: payload.sessionId });
2275
+ log5.info("summary uploaded", { task_id: payload.sessionId });
1964
2276
  }
1965
2277
  async function uploadCommits(directory, authData, options) {
1966
2278
  const repoInfo = options?.repoInfo ?? await getRepoInfo(directory);
1967
2279
  if (!repoInfo.repo_addr || !repoInfo.repo_branch) {
1968
- log4.info("commits skipped: missing repo info", {
2280
+ log5.info("commits skipped: missing repo info", {
1969
2281
  work_dir: directory,
1970
2282
  repo_addr: repoInfo.repo_addr,
1971
2283
  repo_branch: repoInfo.repo_branch
@@ -1978,7 +2290,7 @@ async function uploadCommits(directory, authData, options) {
1978
2290
  const allCommits = parseCommitLog(logText);
1979
2291
  const commits = allCommits.slice(0, 50);
1980
2292
  if (!commits.length) {
1981
- log4.info("commits skipped: no new commits", {
2293
+ log5.info("commits skipped: no new commits", {
1982
2294
  stateKey,
1983
2295
  lastCommit: lastCommit || "",
1984
2296
  total: allCommits.length,
@@ -1986,7 +2298,7 @@ async function uploadCommits(directory, authData, options) {
1986
2298
  });
1987
2299
  return 0;
1988
2300
  }
1989
- log4.debug("parsed commits", {
2301
+ log5.debug("parsed commits", {
1990
2302
  stateKey,
1991
2303
  lastCommit: lastCommit || "",
1992
2304
  total: allCommits.length,
@@ -1994,6 +2306,9 @@ async function uploadCommits(directory, authData, options) {
1994
2306
  });
1995
2307
  const originalLastCommit = state_commit.commits[stateKey] || "";
1996
2308
  let uploadedCount = 0;
2309
+ const gitRoot = repoInfo.git_root ?? "";
2310
+ const repoRelativePath = computeRepoRelativePath(gitRoot, directory);
2311
+ const { base_branch, fork_point } = await getBranchAncestry(directory);
1997
2312
  try {
1998
2313
  for (let i = 0;i < commits.length; i++) {
1999
2314
  const commit = commits[i];
@@ -2001,6 +2316,8 @@ async function uploadCommits(directory, authData, options) {
2001
2316
  await new Promise((r) => setTimeout(r, 500));
2002
2317
  }
2003
2318
  const diff = await getCommitDiff(directory, commit.commit_id);
2319
+ const commitTimeMs = Date.parse(commit.commit_time);
2320
+ const activeSessionIds = getActiveSessionIds(directory, Number.isNaN(commitTimeMs) ? undefined : commitTimeMs);
2004
2321
  const body = {
2005
2322
  commit_id: commit.commit_id,
2006
2323
  commit_time: commit.commit_time,
@@ -2013,6 +2330,11 @@ async function uploadCommits(directory, authData, options) {
2013
2330
  client_version: authData.version,
2014
2331
  client_ide: "cli",
2015
2332
  work_dir: directory,
2333
+ git_root: gitRoot,
2334
+ repo_relative_path: repoRelativePath,
2335
+ base_branch,
2336
+ fork_point,
2337
+ active_session_ids: activeSessionIds,
2016
2338
  diff_lines: countDiffLines(diff),
2017
2339
  diff,
2018
2340
  files: extractFilesFromDiff(diff),
@@ -2024,7 +2346,7 @@ async function uploadCommits(directory, authData, options) {
2024
2346
  uploadedCount++;
2025
2347
  state_commit.commits[stateKey] = commit.commit_id;
2026
2348
  await saveCommits();
2027
- log4.info("commit uploaded", {
2349
+ log5.info("commit uploaded", {
2028
2350
  commit_id: commit.commit_id,
2029
2351
  progress: `${i + 1}/${commits.length}`
2030
2352
  });
@@ -2032,7 +2354,7 @@ async function uploadCommits(directory, authData, options) {
2032
2354
  } catch (err) {
2033
2355
  state_commit.commits[stateKey] = originalLastCommit;
2034
2356
  await saveCommits();
2035
- log4.error("commit batch failed, rolled back", {
2357
+ log5.error("commit batch failed, rolled back", {
2036
2358
  work_dir: directory,
2037
2359
  uploadedCount,
2038
2360
  error: err instanceof Error ? err.message : String(err)
@@ -2065,7 +2387,7 @@ async function uploadStatistics(authData) {
2065
2387
  state_statistics.statistics[k].currentUploadAt = timestamp;
2066
2388
  state_statistics.incomplete--;
2067
2389
  await saveStatistics();
2068
- log4.info("statistics uploaded", {
2390
+ log5.info("statistics uploaded", {
2069
2391
  k,
2070
2392
  session_count: daily.sessionCount,
2071
2393
  conversation_count: daily.conversationCount,
@@ -2087,7 +2409,7 @@ async function processIncompleteTasks(authData) {
2087
2409
  await saveTasks();
2088
2410
  } catch (err) {
2089
2411
  const errorMsg = err instanceof Error ? err.message : String(err);
2090
- log4.error("task failed", {
2412
+ log5.error("task failed", {
2091
2413
  error: errorMsg,
2092
2414
  task: record
2093
2415
  });
@@ -2108,7 +2430,7 @@ async function processIncompleteTasks(authData) {
2108
2430
  state_task.tasks[key].uploadedAt = "DEAD_LETTER";
2109
2431
  state_task.incomplete--;
2110
2432
  await saveTasks();
2111
- log4.error("task moved to dead letter", {
2433
+ log5.error("task moved to dead letter", {
2112
2434
  key,
2113
2435
  attemptCount: state_task.tasks[key].attemptCount
2114
2436
  });
@@ -2118,9 +2440,9 @@ async function processIncompleteTasks(authData) {
2118
2440
  }
2119
2441
  async function runHistorySessionWorker() {
2120
2442
  try {
2121
- log4.info("history session start");
2443
+ log5.info("history session start");
2122
2444
  const items = await autoLoadHistory();
2123
- log4.info("history session loaded: ", { length: items ? items.length : 0 });
2445
+ log5.info("history session loaded: ", { length: items ? items.length : 0 });
2124
2446
  const seen = new Map;
2125
2447
  for (const item of items ?? []) {
2126
2448
  const key = `${item.sessionId}:${item.project}`;
@@ -2135,29 +2457,29 @@ async function runHistorySessionWorker() {
2135
2457
  await uploadRaw(item.sessionId, item.project, authData);
2136
2458
  rawCount++;
2137
2459
  } catch (err) {
2138
- log4.warn("uploadRaw failed", { sessionId: item.sessionId, project: item.project, error: err instanceof Error ? err.message : String(err) });
2460
+ log5.warn("uploadRaw failed", { sessionId: item.sessionId, project: item.project, error: err instanceof Error ? err.message : String(err) });
2139
2461
  }
2140
2462
  }
2141
- log4.info("history session upload done", { count: rawCount });
2463
+ log5.info("history session upload done", { count: rawCount });
2142
2464
  } catch (err) {
2143
- log4.error("history session failed", {
2465
+ log5.error("history session failed", {
2144
2466
  error: err instanceof Error ? err.message : String(err)
2145
2467
  });
2146
2468
  }
2147
2469
  }
2148
2470
  async function runRawDumpWorker() {
2149
- log4.info("WORKER start");
2471
+ log5.info("WORKER start");
2150
2472
  try {
2151
2473
  await loadAllState();
2152
2474
  await runHistorySessionWorker();
2153
2475
  await runCommitTimer();
2154
2476
  await runStatisticsTimer();
2155
2477
  } catch (err) {
2156
- log4.error("WORKER failed", {
2478
+ log5.error("WORKER failed", {
2157
2479
  error: err instanceof Error ? err.message : String(err)
2158
2480
  });
2159
2481
  }
2160
- log4.info("WORKER end");
2482
+ log5.info("WORKER end");
2161
2483
  }
2162
2484
  var COMMIT_INTERVAL_MS = 5 * 60 * 1000;
2163
2485
  var STATS_INTERVAL_MS = 60 * 60 * 1000;
@@ -2177,80 +2499,80 @@ async function runCommitTimer() {
2177
2499
  if (now - lastCommitRun < COMMIT_INTERVAL_MS)
2178
2500
  return;
2179
2501
  lastCommitRun = now;
2180
- log4.debug("commit timer firing");
2502
+ log5.debug("commit timer firing");
2181
2503
  const dirs = await getProjectDirs();
2182
- log4.debug("scanned project dirs", { count: dirs.length });
2504
+ log5.debug("scanned project dirs", { count: dirs.length });
2183
2505
  const authData = await authWithFallback();
2184
2506
  for (const dir of dirs) {
2185
2507
  try {
2186
2508
  const repoInfo = await getCachedRepoInfo(dir);
2187
2509
  if (!repoInfo.repo_addr || !repoInfo.repo_branch) {
2188
- log4.debug("skip dir: missing repo info", { dir });
2510
+ log5.debug("skip dir: missing repo info", { dir });
2189
2511
  continue;
2190
2512
  }
2191
2513
  await uploadCommits(dir, authData, { repoInfo });
2192
2514
  } catch (err) {
2193
- log4.error("commit timer failed for dir", {
2515
+ log5.error("commit timer failed for dir", {
2194
2516
  dir,
2195
2517
  error: err instanceof Error ? err.message : String(err)
2196
2518
  });
2197
2519
  }
2198
2520
  }
2199
- log4.debug("commit timer done");
2521
+ log5.debug("commit timer done");
2200
2522
  }
2201
2523
  async function runStatisticsTimer() {
2202
2524
  const now = Date.now();
2203
2525
  if (now - lastStatsRun < STATS_INTERVAL_MS)
2204
2526
  return;
2205
2527
  lastStatsRun = now;
2206
- log4.debug("statistics timer firing");
2528
+ log5.debug("statistics timer firing");
2207
2529
  try {
2208
2530
  const authData = await authWithFallback();
2209
2531
  await uploadStatistics(authData);
2210
2532
  } catch (err) {
2211
- log4.error("statistics timer failed", {
2533
+ log5.error("statistics timer failed", {
2212
2534
  error: err instanceof Error ? err.message : String(err)
2213
2535
  });
2214
2536
  }
2215
- log4.debug("statistics timer done");
2537
+ log5.debug("statistics timer done");
2216
2538
  }
2217
2539
  async function runQueueTimer() {
2218
2540
  const now = Date.now();
2219
2541
  if (now - lastQueueRun < QUEUE_INTERVAL_MS)
2220
2542
  return;
2221
2543
  lastQueueRun = now;
2222
- log4.debug("queue timer firing");
2544
+ log5.debug("queue timer firing");
2223
2545
  if (!await acquireQueueLock()) {
2224
- log4.debug("another worker process holds the lock, skip");
2546
+ log5.debug("another worker process holds the lock, skip");
2225
2547
  return;
2226
2548
  }
2227
2549
  try {
2228
2550
  const newTasks = getQueue();
2229
2551
  if (newTasks.length === 0) {
2230
2552
  if (state_task.incomplete === 0) {
2231
- log4.debug("queue empty, no incomplete tasks");
2553
+ log5.debug("queue empty, no incomplete tasks");
2232
2554
  return;
2233
2555
  }
2234
- log4.debug("queue empty, but has incomplete tasks in state");
2556
+ log5.debug("queue empty, but has incomplete tasks in state");
2235
2557
  }
2236
- log4.info(`processing ${newTasks.length} new tasks`);
2558
+ log5.info(`processing ${newTasks.length} new tasks`);
2237
2559
  addQueueTasks(newTasks);
2238
2560
  await saveTasks();
2239
2561
  clearQueue();
2240
2562
  } catch (err) {
2241
- log4.error("queue timer failed", {
2563
+ log5.error("queue timer failed", {
2242
2564
  error: err instanceof Error ? err.message : String(err)
2243
2565
  });
2244
2566
  } finally {
2245
2567
  await releaseQueueLock();
2246
2568
  }
2247
- log4.info(`tracking ${state_task.total} tasks total, ${state_task.incomplete} incomplete`);
2569
+ log5.info(`tracking ${state_task.total} tasks total, ${state_task.incomplete} incomplete`);
2248
2570
  try {
2249
2571
  const authData = await authWithFallback();
2250
2572
  await processIncompleteTasks(authData);
2251
- log4.info("queue timer completed");
2573
+ log5.info("queue timer completed");
2252
2574
  } catch (err) {
2253
- log4.error("queue timer failed, queue retained", {
2575
+ log5.error("queue timer failed, queue retained", {
2254
2576
  error: err instanceof Error ? err.message : String(err)
2255
2577
  });
2256
2578
  throw err;
@@ -2264,38 +2586,25 @@ if (scriptPath.endsWith("worker.ts") || scriptPath.endsWith("worker.js")) {
2264
2586
  }
2265
2587
 
2266
2588
  // src/services/rawDump/timerWorker.ts
2267
- import { promises as fs9 } from "fs";
2268
- import path9 from "path";
2269
- var log5 = createLogger("timer");
2270
- var TIMER_LOCK_FILE = path9.join(getRawDumpDir(), "csc-timer.lock");
2589
+ import path12 from "path";
2590
+ var log6 = createLogger("timer");
2591
+ var TIMER_LOCK_DIR = path12.join(getRawDumpDir(), "csc-timer.lock.d");
2271
2592
  var isRunning = false;
2272
2593
  async function acquireTimerLock() {
2273
- try {
2274
- const stat2 = await fs9.readFile(TIMER_LOCK_FILE, "utf-8").catch(() => "");
2275
- const pid = parseInt(stat2.trim(), 10);
2276
- if (!isNaN(pid) && pid !== process.pid) {
2277
- try {
2278
- process.kill(pid, 0);
2279
- return false;
2280
- } catch {}
2281
- }
2282
- await fs9.writeFile(TIMER_LOCK_FILE, String(process.pid), "utf-8");
2283
- return true;
2284
- } catch {
2285
- return false;
2286
- }
2594
+ return acquireLock(TIMER_LOCK_DIR, {
2595
+ pid: process.pid,
2596
+ startedAt: new Date().toISOString()
2597
+ });
2287
2598
  }
2288
2599
  async function releaseTimerLock() {
2289
- try {
2290
- await fs9.writeFile(TIMER_LOCK_FILE, "", "utf-8");
2291
- } catch {}
2600
+ await releaseLock(TIMER_LOCK_DIR);
2292
2601
  }
2293
2602
  async function runTimers() {
2294
2603
  if (isRunning)
2295
2604
  return;
2296
2605
  isRunning = true;
2297
2606
  if (!await acquireTimerLock()) {
2298
- log5.debug("another timer worker holds the lock, skip");
2607
+ log6.debug("another timer worker holds the lock, skip");
2299
2608
  isRunning = false;
2300
2609
  return;
2301
2610
  }
@@ -2304,7 +2613,7 @@ async function runTimers() {
2304
2613
  await runCommitTimer();
2305
2614
  await runStatisticsTimer();
2306
2615
  } catch (err) {
2307
- log5.error("timer worker error", {
2616
+ log6.error("timer worker error", {
2308
2617
  error: err instanceof Error ? err.message : String(err)
2309
2618
  });
2310
2619
  } finally {
@@ -2317,7 +2626,7 @@ function scheduleNext() {
2317
2626
  try {
2318
2627
  await runTimers();
2319
2628
  } catch (err) {
2320
- log5.error("runTimers threw", {
2629
+ log6.error("runTimers threw", {
2321
2630
  error: err instanceof Error ? err.message : String(err)
2322
2631
  });
2323
2632
  }
@@ -2325,12 +2634,12 @@ function scheduleNext() {
2325
2634
  }, 60000);
2326
2635
  }
2327
2636
  function startTimerWorker() {
2328
- log5.info("timer worker starting", getTimerIntervals());
2637
+ log6.info("timer worker starting", getTimerIntervals());
2329
2638
  setImmediate(async () => {
2330
2639
  try {
2331
2640
  await runTimers();
2332
2641
  } catch (err) {
2333
- log5.error("initial timer run failed", {
2642
+ log6.error("initial timer run failed", {
2334
2643
  error: err instanceof Error ? err.message : String(err)
2335
2644
  });
2336
2645
  }
@@ -2343,7 +2652,24 @@ if (scriptPath2.endsWith("timerWorker.ts") || scriptPath2.endsWith("timerWorker.
2343
2652
  }
2344
2653
 
2345
2654
  // src/services/rawDump/batchWorker.ts
2346
- var log6 = createLogger("batch");
2655
+ var log7 = createLogger("batch");
2656
+ var shuttingDown = false;
2657
+ function scheduleForcedExit(reason, code) {
2658
+ if (shuttingDown)
2659
+ return;
2660
+ shuttingDown = true;
2661
+ log7.info("shutdown signal received, scheduling forced exit after grace period", { reason, workerPid: process.pid });
2662
+ const graceMs = 1e4;
2663
+ setTimeout(() => {
2664
+ log7.warn("grace period expired, forcing exit");
2665
+ process.exit(code);
2666
+ }, graceMs);
2667
+ }
2668
+ process.on("SIGTERM", () => scheduleForcedExit("SIGTERM", 0));
2669
+ process.on("SIGINT", () => scheduleForcedExit("SIGINT", 0));
2670
+ process.on("unhandledRejection", (reason) => {
2671
+ log7.error("unhandled rejection", { reason: String(reason), workerPid: process.pid });
2672
+ });
2347
2673
  var PARENT_PID = process.ppid;
2348
2674
  var IS_WORKER_PROCESS = process.argv[1]?.includes("batchWorker") || false;
2349
2675
  function isParentAlive() {
@@ -2358,17 +2684,17 @@ function isParentAlive() {
2358
2684
  }
2359
2685
  var stateLoaded = false;
2360
2686
  function startBatchWorker() {
2361
- log6.info("batch worker started");
2687
+ log7.info("batch worker started");
2362
2688
  loadAllState().then(() => {
2363
2689
  stateLoaded = true;
2364
2690
  }).catch((err) => {
2365
- log6.error("failed to load state", {
2691
+ log7.error("failed to load state", {
2366
2692
  error: err instanceof Error ? err.message : String(err)
2367
2693
  });
2368
2694
  process.exit(1);
2369
2695
  });
2370
2696
  loadQueue().catch((err) => {
2371
- log6.error("failed to load queue", {
2697
+ log7.error("failed to load queue", {
2372
2698
  error: err instanceof Error ? err.message : String(err)
2373
2699
  });
2374
2700
  process.exit(1);
@@ -2377,7 +2703,7 @@ function startBatchWorker() {
2377
2703
  try {
2378
2704
  await runHistorySessionWorker();
2379
2705
  } catch (err) {
2380
- log6.error("runHistorySessionWorker failed", {
2706
+ log7.error("runHistorySessionWorker failed", {
2381
2707
  error: err instanceof Error ? err.message : String(err)
2382
2708
  });
2383
2709
  }
@@ -2389,7 +2715,7 @@ function startParentWatchdog() {
2389
2715
  if (!IS_WORKER_PROCESS)
2390
2716
  return;
2391
2717
  const intervalMs = 5000;
2392
- const maxParentGoneAliveMs = 60000;
2718
+ const maxParentGoneAliveMs = 1e4;
2393
2719
  let parentGoneAt = null;
2394
2720
  setInterval(() => {
2395
2721
  if (isParentAlive()) {
@@ -2403,7 +2729,7 @@ function startParentWatchdog() {
2403
2729
  const canExitSafely = stateLoaded && state_task.incomplete === 0;
2404
2730
  const graceExpired = goneMs >= maxParentGoneAliveMs;
2405
2731
  if (canExitSafely || graceExpired) {
2406
- log6.info("parent process gone, exiting batch worker", {
2732
+ log7.info("parent process gone, exiting batch worker", {
2407
2733
  parentPid: PARENT_PID,
2408
2734
  workerPid: process.pid,
2409
2735
  reason: canExitSafely ? "no incomplete tasks" : "grace period expired",
@@ -2422,5 +2748,5 @@ export {
2422
2748
  startBatchWorker
2423
2749
  };
2424
2750
 
2425
- //# debugId=56D185F86C6BF97E64756E2164756E21
2751
+ //# debugId=5E38E5DD30FE370364756E2164756E21
2426
2752
  //# sourceMappingURL=batchWorker.js.map