@ascenda-one/history-import 0.1.14 → 0.1.15

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
@@ -30,8 +30,15 @@ command (same pattern as hooks pairing) and this CLI does the reading.
30
30
  4. **Metrics only by default.** Prompt/response text, thinking blocks and
31
31
  file contents never leave the machine. Content-level ingestion, if it ever
32
32
  ships, is a separate explicit opt-in — not this package's default path.
33
- 5. **Aggregate before shipping.** Per-session / per-day events, not one event
34
- per bubble — a single machine's stores hold tens of thousands of them.
33
+ 5. **Aggregate before shipping, unless a reader counts the rows.** Per-session
34
+ / per-day events, not one event per bubble — a single machine's stores hold
35
+ tens of thousands of them. The one deliberate exception is
36
+ `ai_tool_call_started`: the backend's work-demand rail derives
37
+ `toolCallCount` by counting rows of that type and reads no `toolCallCount`
38
+ key off metadata, so a session-level aggregate would ship, store, and be
39
+ counted by nothing. Per-call events also place the work in the right hour,
40
+ which a session spanning six of them cannot. Expect an order of magnitude
41
+ more events than a session-only import, and an `events.jsonl` to match.
35
42
  6. **Provenance is data.** Every event carries `historical_direct`,
36
43
  `historical_derived` or `historical_unparsed` — never the live
37
44
  `ai_work_telemetry` provenance — so no chart can pass history off as
@@ -46,6 +53,7 @@ command (same pattern as hooks pairing) and this CLI does the reading.
46
53
  | Staging/snapshot (copy-then-parse, WAL-aware, **torn down by the run that makes it**) | implemented |
47
54
  | `archive` (durable content-addressed copy, dedup, verify, restore, prune) | **implemented, verified on a real 4.1 GB store** |
48
55
  | **Claude Code extractor** (human-prompt/tool-result split, session folds incl. recursive subagent transcripts, after-hours, compaction, tool failures, context-window peak, human-corrected edits, correction cadence, gap-split active minutes, epoch marker) | **implemented, verified live** |
56
+ | **Tool-call counting, all three stores** (`tool_use` items / `toolFormerData` / `toolInvocationSerialized`, one `ai_tool_call_started` per call) | **implemented; exercised end to end against all three stores on a developer machine** |
49
57
  | **Batch shipper** (`POST /v1/tool-events/batch`, salted hashes, stable importKey) | **implemented, verified live** |
50
58
  | **Cursor extractor** (composerHeaders + bubble aggregation via SQL-side `json_extract`, prompt text never parsed into the process, subagent-composer folding, epoch marker) | **implemented, verified live** |
51
59
  | **VS Code extractor** (Timeline-history Chat-Edit day×workspace aggregation, Copilot chatSessions folding, workspace identity via `workspace.json` longest-prefix match, epoch marker) | **implemented, verified live** |
@@ -119,7 +127,7 @@ ascenda-history-import archive # the durable copy; --verify / --list / -
119
127
  ### Staging is scaffolding; `archive` is the copy
120
128
 
121
129
  `import` snapshots each store, extracts, and **deletes the snapshot in a
122
- `finally`** — success or failure — keeping only the ~10 MB `events.jsonl`.
130
+ `finally`** — success or failure — keeping only `events.jsonl`.
123
131
  It also sweeps snapshots left by earlier runs. This is not tidiness: nineteen
124
132
  runs once left 254 GB on a 926 GB disk and took free space to 279 MB, and the
125
133
  first thing to notice was unrelated tooling failing with `ENOSPC`.
package/dist/cli.js CHANGED
@@ -377,12 +377,155 @@ var require_payload = __commonJS({
377
377
  }
378
378
  });
379
379
 
380
+ // ../packages/tool-contract/out/metricKeys.js
381
+ var require_metricKeys = __commonJS({
382
+ "../packages/tool-contract/out/metricKeys.js"(exports) {
383
+ "use strict";
384
+ Object.defineProperty(exports, "__esModule", { value: true });
385
+ exports.METRIC_KEYS = void 0;
386
+ exports.backendMetricKeys = backendMetricKeys;
387
+ var CONTEXT_WINDOW_CANONICAL_ALIASES = [
388
+ "contextWindowPeakPct",
389
+ "context_window_peak_pct",
390
+ "contextWindowPct",
391
+ "context_window_pct"
392
+ ];
393
+ var CONTEXT_WINDOW_CURSOR_PERCENT_ALIASES = ["contextUsagePercent"];
394
+ exports.METRIC_KEYS = {
395
+ // ── Read by a backend reader ────────────────────────────────────────────
396
+ contextWindowPeakPct: {
397
+ readBy: ["backend", "handoff"],
398
+ backendAliases: CONTEXT_WINDOW_CANONICAL_ALIASES,
399
+ unit: "fraction of the context window (0\u20131; uncapped for >200k contexts)",
400
+ note: "Claude Code reports a true per-session peak. Cursor reports its composer's last known occupancy under the same key \u2014 the closest its store can answer, and not the same measurement."
401
+ },
402
+ contextUsagePercent: {
403
+ readBy: ["backend", "handoff"],
404
+ backendAliases: CONTEXT_WINDOW_CURSOR_PERCENT_ALIASES,
405
+ unit: "percent (0\u2013100)",
406
+ note: "Cursor's own column name. Superseded by contextWindowPeakPct on the wire; kept because the handoff reads it and imported rows carry it. Unit-explicit on the backend: always divided by 100, never put through the fraction-or-percent heuristic."
407
+ },
408
+ promptCount: { readBy: ["backend", "handoff"], backendAliases: ["promptCount", "prompt_count"] },
409
+ sessionMinutes: { readBy: ["backend"], backendAliases: ["sessionMinutes", "session_minutes"], unit: "minutes" },
410
+ durationBucket: { readBy: ["backend", "handoff"], backendAliases: ["durationBucket", "duration_bucket"] },
411
+ afterHoursPrompts: { readBy: ["backend", "handoff"], backendAliases: ["afterHoursPrompts", "after_hours_prompts"] },
412
+ inputTokens: { readBy: ["backend"], backendAliases: ["inputTokens", "input_tokens"], unit: "tokens" },
413
+ outputTokens: { readBy: ["backend"], backendAliases: ["outputTokens", "output_tokens"], unit: "tokens" },
414
+ cacheReadTokens: { readBy: ["backend"], backendAliases: ["cacheReadTokens", "cache_read_tokens"], unit: "tokens" },
415
+ queuedPrompts: { readBy: ["backend"], backendAliases: ["queuedPrompts", "queued_prompts"] },
416
+ linesChangedBucket: { readBy: ["backend"], backendAliases: ["linesChangedBucket", "lines_changed_bucket"] },
417
+ // ── Read by the local handoff only ──────────────────────────────────────
418
+ activeMinutes: { readBy: ["handoff"], unit: "minutes" },
419
+ afterHoursRequests: { readBy: ["handoff"] },
420
+ approximateLintErrorsCount: { readBy: ["handoff"] },
421
+ canceledCount: { readBy: ["handoff"] },
422
+ chatEditCount: { readBy: ["handoff"] },
423
+ compactionCount: {
424
+ readBy: ["handoff"],
425
+ note: "The backend counts context_compression_* event rows, not this key. Both are emitted; this one is for the handoff."
426
+ },
427
+ contextWindowPeakTokens: {
428
+ readBy: ["handoff"],
429
+ unit: "tokens",
430
+ note: "The measured quantity, with no assumed denominator. Prefer this to the ratio for any within-person baseline."
431
+ },
432
+ date: { readBy: ["handoff"] },
433
+ errorCount: { readBy: ["handoff"] },
434
+ filesChangedCount: { readBy: ["handoff"] },
435
+ humanChangesCount: { readBy: ["handoff"] },
436
+ linesAdded: { readBy: ["handoff"] },
437
+ linesRemoved: { readBy: ["handoff"] },
438
+ primaryModel: { readBy: ["handoff"] },
439
+ requestCount: { readBy: ["handoff"] },
440
+ sessionStartedAt: { readBy: ["handoff"] },
441
+ subagentComposers: { readBy: ["handoff"] },
442
+ subagentToolCallCount: {
443
+ readBy: ["handoff"],
444
+ note: "Claude Code only: calls made inside subagent transcripts, kept out of toolCallCount so the main-loop count matches what the wire events carry."
445
+ },
446
+ subagentTranscripts: { readBy: ["handoff"] },
447
+ toolCallCount: {
448
+ readBy: ["handoff"],
449
+ note: "The backend counts ai_tool_call_started event rows, not this key \u2014 same split as compactionCount. Deduplicated on the tool-call id; see each extractor for what one call means in its store."
450
+ },
451
+ toolCallsUndated: {
452
+ readBy: ["handoff"],
453
+ note: "Cursor only: calls whose every record carries an empty createdAt. Counted in toolCallCount, but no event exists for them \u2014 the wire total is smaller than this session count by exactly this number."
454
+ },
455
+ toolFailureCount: { readBy: ["handoff"] },
456
+ totalEntryCount: { readBy: ["handoff"] },
457
+ userModifiedEditCount: {
458
+ readBy: ["handoff"],
459
+ note: "Null, never 0 \u2014 Claude Code never sets userModified true, so 0 would assert 'no AI edit was ever corrected by hand'."
460
+ },
461
+ // ── Diagnostic: read by nobody, on purpose ──────────────────────────────
462
+ abandonedPromptCount: { readBy: ["diagnostic"] },
463
+ // Epoch-marker metrics. The marker is local-only and never reaches the wire
464
+ // (see EXTRACTION_EPOCH_KIND), but it travels as a NormalizedHistoricalEvent
465
+ // and so is keyed by the same vocabulary.
466
+ windowOldest: { readBy: ["handoff"], note: "Oldest event the extraction saw \u2014 the marker's left edge." },
467
+ windowNewest: { readBy: ["handoff"], note: "Newest event the extraction saw \u2014 the marker's right edge." },
468
+ projectsWithNoReadableTranscript: { readBy: ["diagnostic"] },
469
+ unparsedComposerHeaders: { readBy: ["diagnostic"] },
470
+ unknownComposerHeaderTypes: { readBy: ["diagnostic"] },
471
+ orphanedBubbles: { readBy: ["diagnostic"] },
472
+ orphanedSubagentBubbles: { readBy: ["diagnostic"] },
473
+ sessionsWithoutTimeline: { readBy: ["diagnostic"] },
474
+ emptyComposers: { readBy: ["diagnostic"] },
475
+ apiErrorCount: { readBy: ["diagnostic"] },
476
+ assistantTurns: { readBy: ["diagnostic"] },
477
+ compactionAutoCount: { readBy: ["diagnostic"] },
478
+ compactionManualCount: { readBy: ["diagnostic"] },
479
+ editDayCount: { readBy: ["diagnostic"] },
480
+ emptyChatSessions: { readBy: ["diagnostic"] },
481
+ gitBranch: { readBy: ["diagnostic"] },
482
+ linesChanged: { readBy: ["diagnostic"] },
483
+ malformedChatSessionLines: { readBy: ["diagnostic"] },
484
+ malformedHistoryEntries: { readBy: ["diagnostic"] },
485
+ mode: { readBy: ["diagnostic"] },
486
+ modelCount: { readBy: ["diagnostic"] },
487
+ modelSwitchCount: { readBy: ["diagnostic"] },
488
+ rapidRepromptCount: { readBy: ["diagnostic"] },
489
+ schemaUnreadable: { readBy: ["diagnostic"] },
490
+ sessionCount: { readBy: ["diagnostic"] },
491
+ sessionsFromBubbleTimeline: { readBy: ["diagnostic"] },
492
+ sessionsFromCheckpointTimeline: { readBy: ["diagnostic"] },
493
+ sessionsFromRecencyTimeline: { readBy: ["diagnostic"] },
494
+ subagentAssistantTurns: { readBy: ["diagnostic"] },
495
+ subagentPrompts: { readBy: ["diagnostic"] },
496
+ subagentTokensTotal: { readBy: ["diagnostic"] },
497
+ toolName: {
498
+ readBy: ["diagnostic"],
499
+ note: "Set per ai_tool_call_started event by #43's extractors and shipped in wire metadata, but no reader resolves it server-side yet (the backend's ToolName column is MCP audit, not telemetry). Registered after the fact: #38 and #43 merged past each other, and the union caught it on the next compile \u2014 metaLines all over again."
500
+ },
501
+ toolResultCount: { readBy: ["diagnostic"] },
502
+ toolResultErrorCount: { readBy: ["diagnostic"] },
503
+ unknownBubbles: { readBy: ["diagnostic"] },
504
+ unknownLines: { readBy: ["diagnostic"] },
505
+ metaLines: {
506
+ readBy: ["diagnostic"],
507
+ note: 'Recognised-but-skipped transcript machinery (file-history-snapshot, queued-command, \u2026). Split out of unknownLines by #43 so that number keeps meaning "a type nobody has looked at". Registered here after the fact: #41 and #43 merged past each other, and the union caught it on the next compile \u2014 which is this module doing its job.'
508
+ },
509
+ unparsedBubbles: { readBy: ["diagnostic"] },
510
+ unparsedChatSessionFiles: { readBy: ["diagnostic"] },
511
+ unparsedHistoryFiles: { readBy: ["diagnostic"] },
512
+ unparsedLines: { readBy: ["diagnostic"] },
513
+ unreadableChatSessionFiles: { readBy: ["diagnostic"] },
514
+ unreadableHistoryFiles: { readBy: ["diagnostic"] },
515
+ unrecognisedChatSessionFiles: { readBy: ["diagnostic"] }
516
+ };
517
+ function backendMetricKeys() {
518
+ return Object.entries(exports.METRIC_KEYS).filter(([, spec]) => spec.readBy.includes("backend")).map(([key, spec]) => [key, spec.backendAliases ?? [key]]);
519
+ }
520
+ }
521
+ });
522
+
380
523
  // ../packages/tool-contract/out/index.js
381
524
  var require_out = __commonJS({
382
525
  "../packages/tool-contract/out/index.js"(exports) {
383
526
  "use strict";
384
527
  Object.defineProperty(exports, "__esModule", { value: true });
385
- exports.ASCENDA_SEMANTIC_PROVENANCE = exports.ASCENDA_HISTORICAL_CONSENT_SCOPE = exports.ASCENDA_COLLABORATION_CONSENT_SCOPE = exports.ASCENDA_SEMANTIC_CONSENT_SCOPE = exports.ASCENDA_PROVENANCE = exports.ASCENDA_CONSENT_SCOPE = exports.EVENT_WORKLOAD_CATEGORY = exports.COLLABORATION_EVENT_TYPES = exports.SEMANTIC_WORK_SIGNAL_EVENT_TYPES = void 0;
528
+ exports.backendMetricKeys = exports.METRIC_KEYS = exports.ASCENDA_SEMANTIC_PROVENANCE = exports.ASCENDA_HISTORICAL_CONSENT_SCOPE = exports.ASCENDA_COLLABORATION_CONSENT_SCOPE = exports.ASCENDA_SEMANTIC_CONSENT_SCOPE = exports.ASCENDA_PROVENANCE = exports.ASCENDA_CONSENT_SCOPE = exports.EVENT_WORKLOAD_CATEGORY = exports.COLLABORATION_EVENT_TYPES = exports.SEMANTIC_WORK_SIGNAL_EVENT_TYPES = void 0;
386
529
  exports.SEMANTIC_WORK_SIGNAL_EVENT_TYPES = [
387
530
  "approach_churn_detected",
388
531
  "goal_drift_detected",
@@ -441,6 +584,13 @@ var require_out = __commonJS({
441
584
  exports.ASCENDA_COLLABORATION_CONSENT_SCOPE = "workflow_telemetry";
442
585
  exports.ASCENDA_HISTORICAL_CONSENT_SCOPE = "historical_import";
443
586
  exports.ASCENDA_SEMANTIC_PROVENANCE = "semantic_work_signals";
587
+ var metricKeys_1 = require_metricKeys();
588
+ Object.defineProperty(exports, "METRIC_KEYS", { enumerable: true, get: function() {
589
+ return metricKeys_1.METRIC_KEYS;
590
+ } });
591
+ Object.defineProperty(exports, "backendMetricKeys", { enumerable: true, get: function() {
592
+ return metricKeys_1.backendMetricKeys;
593
+ } });
444
594
  }
445
595
  });
446
596
 
@@ -675,6 +825,7 @@ var require_tokenStore = __commonJS({
675
825
  };
676
826
  }();
677
827
  Object.defineProperty(exports, "__esModule", { value: true });
828
+ exports.ascendaHome = ascendaHome;
678
829
  exports.defaultTokenFilePath = defaultTokenFilePath2;
679
830
  exports.persistEventWriteToken = persistEventWriteToken;
680
831
  exports.readTokenFile = readTokenFile2;
@@ -682,8 +833,11 @@ var require_tokenStore = __commonJS({
682
833
  var fs10 = __importStar(__require("fs"));
683
834
  var os3 = __importStar(__require("os"));
684
835
  var path10 = __importStar(__require("path"));
836
+ function ascendaHome() {
837
+ return process.env.ASCENDA_HOME ?? path10.join(os3.homedir(), ".ascenda");
838
+ }
685
839
  function defaultTokenFilePath2(toolInstallationId) {
686
- return path10.join(os3.homedir(), ".ascenda", "tokens", sanitizeFilePart(toolInstallationId));
840
+ return path10.join(ascendaHome(), "tokens", sanitizeFilePart(toolInstallationId));
687
841
  }
688
842
  function persistEventWriteToken(tokenFilePath, token) {
689
843
  const dir = path10.dirname(tokenFilePath);
@@ -1723,8 +1877,8 @@ var require_out2 = __commonJS({
1723
1877
  "../packages/tool-kit/out/index.js"(exports) {
1724
1878
  "use strict";
1725
1879
  Object.defineProperty(exports, "__esModule", { value: true });
1726
- exports.workContextRegistryFilePath = exports.readWorkContextRegistry = exports.recordWorkContextAlias = exports.recordWorkContext = exports.deriveWorkContext = exports.hashWithMachineSalt = exports.readOrCreateMachineSalt = exports.machineSaltFilePath = exports.markFailureNotified = exports.shouldAnnounceFailure = exports.recordSendOutcome = exports.readCollectorState = exports.defaultStateFilePath = exports.readTokenFile = exports.persistEventWriteToken = exports.defaultTokenFilePath = exports.recordTurnStart = exports.consumeTurnDurationMs = exports.resolveContextHashes = exports.loadCliAgentConfig = exports.deliverHookEvents = exports.DEFAULT_API_BASE_URL = exports.resolveEventLogPath = exports.expandUserPath = exports.appendEventLog = exports.EVENT_LOG_ENV_VAR = exports.buildEventPayload = exports.AscendaSemanticEventError = exports.AscendaEventSender = exports.looksLikeCorrection = exports.outcomeForHook = exports.inferOutcome = exports.getNestedNumber = exports.getNestedString = exports.getNested = exports.getNumber = exports.getString = exports.localHourAt = exports.utcOffsetMinutesAt = exports.BUSINESS_DAY = exports.isOutsideBusinessHours = exports.isAfterHours = exports.bucketDurationMs = exports.bucketLinesChanged = exports.invitesDebrief = exports.classifyWorkMilestone = exports.isReworkGitAction = exports.classifyGitAction = exports.isVerificationCommand = exports.classifyCommand = void 0;
1727
- exports.isRetryableStatus = exports.parseIngestResponse = exports.postToolEventsBatch = exports.postToolEvent = exports.renewToolToken = exports.getPairingStatus = exports.createPairingSession = exports.AscendaApiError = exports.liveBusSocketCandidates = exports.liveBusSocketPath = exports.bucketPromptSize = exports.emitLiveSignal = void 0;
1880
+ exports.readWorkContextRegistry = exports.recordWorkContextAlias = exports.recordWorkContext = exports.deriveWorkContext = exports.hashWithMachineSalt = exports.readOrCreateMachineSalt = exports.machineSaltFilePath = exports.markFailureNotified = exports.shouldAnnounceFailure = exports.recordSendOutcome = exports.readCollectorState = exports.defaultStateFilePath = exports.readTokenFile = exports.persistEventWriteToken = exports.defaultTokenFilePath = exports.ascendaHome = exports.recordTurnStart = exports.consumeTurnDurationMs = exports.resolveContextHashes = exports.loadCliAgentConfig = exports.deliverHookEvents = exports.DEFAULT_API_BASE_URL = exports.resolveEventLogPath = exports.expandUserPath = exports.appendEventLog = exports.EVENT_LOG_ENV_VAR = exports.buildEventPayload = exports.AscendaSemanticEventError = exports.AscendaEventSender = exports.looksLikeCorrection = exports.outcomeForHook = exports.inferOutcome = exports.getNestedNumber = exports.getNestedString = exports.getNested = exports.getNumber = exports.getString = exports.localHourAt = exports.utcOffsetMinutesAt = exports.BUSINESS_DAY = exports.isOutsideBusinessHours = exports.isAfterHours = exports.bucketDurationMs = exports.bucketLinesChanged = exports.invitesDebrief = exports.classifyWorkMilestone = exports.isReworkGitAction = exports.classifyGitAction = exports.isVerificationCommand = exports.classifyCommand = void 0;
1881
+ exports.isRetryableStatus = exports.parseIngestResponse = exports.postToolEventsBatch = exports.postToolEvent = exports.renewToolToken = exports.getPairingStatus = exports.createPairingSession = exports.AscendaApiError = exports.liveBusSocketCandidates = exports.liveBusSocketPath = exports.bucketPromptSize = exports.emitLiveSignal = exports.workContextRegistryFilePath = void 0;
1728
1882
  var commandClassifier_1 = require_commandClassifier();
1729
1883
  Object.defineProperty(exports, "classifyCommand", { enumerable: true, get: function() {
1730
1884
  return commandClassifier_1.classifyCommand;
@@ -1838,6 +1992,9 @@ var require_out2 = __commonJS({
1838
1992
  return turnState_1.recordTurnStart;
1839
1993
  } });
1840
1994
  var tokenStore_1 = require_tokenStore();
1995
+ Object.defineProperty(exports, "ascendaHome", { enumerable: true, get: function() {
1996
+ return tokenStore_1.ascendaHome;
1997
+ } });
1841
1998
  Object.defineProperty(exports, "defaultTokenFilePath", { enumerable: true, get: function() {
1842
1999
  return tokenStore_1.defaultTokenFilePath;
1843
2000
  } });
@@ -2368,6 +2525,12 @@ var HISTORICAL_PROVENANCE = {
2368
2525
  var HISTORICAL_CONSENT_SCOPE = import_tool_contract.ASCENDA_HISTORICAL_CONSENT_SCOPE;
2369
2526
  var EXTRACTION_EPOCH_KIND = "extraction_epoch";
2370
2527
 
2528
+ // src/toolName.ts
2529
+ function sanitizeToolName(toolName) {
2530
+ if (!toolName) return "unknown";
2531
+ return toolName.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 40) || "unknown";
2532
+ }
2533
+
2371
2534
  // src/daySlice.ts
2372
2535
  var LOCAL_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone ?? null;
2373
2536
  function localDayKey(t) {
@@ -2484,6 +2647,19 @@ function isToolFailureLine(record) {
2484
2647
  (item) => typeof item === "object" && item !== null && item.type === "tool_result" && item.is_error === true
2485
2648
  );
2486
2649
  }
2650
+ function toolUseNamesOf(record) {
2651
+ const message = record.message;
2652
+ const content = message?.content;
2653
+ if (!Array.isArray(content)) return [];
2654
+ const names = [];
2655
+ for (const item of content) {
2656
+ if (typeof item !== "object" || item === null) continue;
2657
+ const entry = item;
2658
+ if (entry.type !== "tool_use") continue;
2659
+ names.push(sanitizeToolName(typeof entry.name === "string" ? entry.name : void 0));
2660
+ }
2661
+ return names;
2662
+ }
2487
2663
  function countChangedLines(structuredPatch) {
2488
2664
  let count = 0;
2489
2665
  for (const hunk of structuredPatch) {
@@ -2531,10 +2707,13 @@ function newFold(sessionId, projectSlug) {
2531
2707
  userModifiedEditCount: 0,
2532
2708
  linesChangedTotal: 0,
2533
2709
  abandonedPromptCount: 0,
2710
+ toolCallCount: 0,
2534
2711
  subagentTranscripts: 0,
2535
2712
  subagentPrompts: 0,
2536
2713
  subagentAssistantTurns: 0,
2537
2714
  subagentTokensTotal: 0,
2715
+ subagentToolCallCount: 0,
2716
+ toolCalls: [],
2538
2717
  timelinePoints: [],
2539
2718
  humanPromptTimestamps: []
2540
2719
  };
@@ -2638,6 +2817,16 @@ async function foldLinesInto(fold, filePath, opts) {
2638
2817
  case "assistant": {
2639
2818
  const record = JSON.parse(line);
2640
2819
  const usage = record.message?.usage;
2820
+ const toolNames = toolUseNamesOf(record);
2821
+ if (toolNames.length > 0) {
2822
+ if (opts.isSidechain) fold.subagentToolCallCount += toolNames.length;
2823
+ else fold.toolCallCount += toolNames.length;
2824
+ if (sniffed.occurredAt) {
2825
+ for (const name of toolNames) {
2826
+ fold.toolCalls.push({ at: sniffed.occurredAt, name });
2827
+ }
2828
+ }
2829
+ }
2641
2830
  if (opts.isSidechain) {
2642
2831
  fold.subagentAssistantTurns += 1;
2643
2832
  if (usage) {
@@ -2796,11 +2985,25 @@ async function* extractClaudeCode(snapshotDir, extractionId) {
2796
2985
  extractionId
2797
2986
  };
2798
2987
  }
2988
+ for (const call of fold.toolCalls) {
2989
+ yield {
2990
+ occurredAt: call.at,
2991
+ store: "claude_code",
2992
+ sourceVersion: fold.sourceVersion,
2993
+ sessionRef: fold.sessionId,
2994
+ repoRef: fold.cwd ?? fold.projectSlug,
2995
+ eventKind: "ai_tool_call_started",
2996
+ metrics: { toolName: call.name },
2997
+ provenance: HISTORICAL_PROVENANCE.direct,
2998
+ extractionId
2999
+ };
3000
+ }
2799
3001
  const durationMs = sessionDurationMs(fold);
2800
3002
  const toolFailureCount = fold.toolResultErrorCount + fold.apiErrorCount;
2801
3003
  const sessionMetrics = {
2802
3004
  promptCount: fold.humanPrompts,
2803
3005
  assistantTurns: fold.assistantTurns,
3006
+ toolCallCount: fold.toolCallCount,
2804
3007
  toolResultCount: fold.toolResults,
2805
3008
  queuedPrompts: fold.queuedPrompts,
2806
3009
  inputTokens: fold.inputTokens,
@@ -2830,7 +3033,8 @@ async function* extractClaudeCode(snapshotDir, extractionId) {
2830
3033
  subagentTranscripts: fold.subagentTranscripts,
2831
3034
  subagentPrompts: fold.subagentPrompts,
2832
3035
  subagentAssistantTurns: fold.subagentAssistantTurns,
2833
- subagentTokensTotal: fold.subagentTokensTotal
3036
+ subagentTokensTotal: fold.subagentTokensTotal,
3037
+ subagentToolCallCount: fold.subagentToolCallCount
2834
3038
  };
2835
3039
  if (durationMs !== null) sessionMetrics.sessionMinutes = Math.round(durationMs / 6e4);
2836
3040
  sessionMetrics.sessionStartedAt = fold.firstTs;
@@ -2996,7 +3200,9 @@ SELECT
2996
3200
  json_extract(value,'$.tokenCount.outputTokens') AS outputTokens,
2997
3201
  coalesce(json_array_length(value,'$.humanChanges'),0) AS humanChangesCount,
2998
3202
  coalesce(json_array_length(value,'$.approximateLintErrors'),0) AS lintErrorsCount,
2999
- json_extract(value,'$.modelInfo.modelName') AS modelName
3203
+ json_extract(value,'$.modelInfo.modelName') AS modelName,
3204
+ json_extract(value,'$.toolFormerData.toolCallId') AS toolCallId,
3205
+ json_extract(value,'$.toolFormerData.name') AS toolName
3000
3206
  FROM cursorDiskKV WHERE key LIKE 'bubbleId:%';
3001
3207
  `;
3002
3208
  function repoInfoOf(row) {
@@ -3057,9 +3263,22 @@ function sniffCursorBubbleRow(row) {
3057
3263
  humanChangesCount: asNumber2(row.humanChangesCount),
3058
3264
  lintErrorsCount: asNumber2(row.lintErrorsCount),
3059
3265
  modelName: row.modelName,
3060
- sourceVersion: String(row.v)
3266
+ sourceVersion: String(row.v),
3267
+ toolCallId: row.toolCallId,
3268
+ toolName: row.toolName
3061
3269
  };
3062
3270
  }
3271
+ function foldToolCall(fold, sniffed) {
3272
+ const id = sniffed.toolCallId;
3273
+ if (!id) return;
3274
+ const at = sniffed.createdAt && sniffed.createdAt.length > 0 ? sniffed.createdAt : null;
3275
+ const known = fold.toolCallFirstSeen.get(id);
3276
+ if (known === void 0) fold.toolCallFirstSeen.set(id, at);
3277
+ else if (at !== null && (known === null || at < known)) fold.toolCallFirstSeen.set(id, at);
3278
+ if (sniffed.toolName && !fold.toolCallNames.has(id)) {
3279
+ fold.toolCallNames.set(id, sanitizeToolName(sniffed.toolName));
3280
+ }
3281
+ }
3063
3282
  function newFold2(row, sniffed) {
3064
3283
  return {
3065
3284
  composerId: row.composerId,
@@ -3088,6 +3307,8 @@ function newFold2(row, sniffed) {
3088
3307
  models: /* @__PURE__ */ new Map(),
3089
3308
  bubbleVersions: /* @__PURE__ */ new Map(),
3090
3309
  humanPromptTimestamps: [],
3310
+ toolCallFirstSeen: /* @__PURE__ */ new Map(),
3311
+ toolCallNames: /* @__PURE__ */ new Map(),
3091
3312
  subagentComposerIds: /* @__PURE__ */ new Set(),
3092
3313
  subagentPromptCount: 0,
3093
3314
  subagentAssistantTurns: 0,
@@ -3211,6 +3432,7 @@ async function* extractCursor(snapshotDir, extractionId) {
3211
3432
  ownFold.lastBubbleMs = ms;
3212
3433
  }
3213
3434
  }
3435
+ foldToolCall(ownFold, sniffed);
3214
3436
  if (sniffed.kind === "user") {
3215
3437
  ownFold.humanPrompts += 1;
3216
3438
  if (sniffed.createdAt && (0, import_tool_kit2.isAfterHours)(new Date(sniffed.createdAt))) {
@@ -3235,6 +3457,7 @@ async function* extractCursor(snapshotDir, extractionId) {
3235
3457
  }
3236
3458
  parentFold.subagentComposerIds.add(sniffed.composerId);
3237
3459
  parentFold.subagentTokensTotal += sniffed.inputTokens + sniffed.outputTokens;
3460
+ foldToolCall(parentFold, sniffed);
3238
3461
  if (sniffed.kind === "user") {
3239
3462
  parentFold.subagentPromptCount += 1;
3240
3463
  } else {
@@ -3281,8 +3504,30 @@ async function* extractCursor(snapshotDir, extractionId) {
3281
3504
  extractionId
3282
3505
  };
3283
3506
  }
3507
+ let toolCallsUndated = 0;
3508
+ for (const [id, at] of [...fold.toolCallFirstSeen].sort((a, b) => a[0].localeCompare(b[0]))) {
3509
+ if (at === null) {
3510
+ toolCallsUndated += 1;
3511
+ continue;
3512
+ }
3513
+ yield {
3514
+ occurredAt: at,
3515
+ store: "cursor",
3516
+ sourceVersion,
3517
+ sessionRef: fold.composerId,
3518
+ repoRef: fold.repoRef,
3519
+ eventKind: "ai_tool_call_started",
3520
+ metrics: { toolName: fold.toolCallNames.get(id) ?? "unknown" },
3521
+ provenance: HISTORICAL_PROVENANCE.direct,
3522
+ extractionId
3523
+ };
3524
+ }
3284
3525
  const durationMs = sessionDurationMs2(fold);
3285
3526
  const sessionMetrics = {
3527
+ toolCallCount: fold.toolCallFirstSeen.size,
3528
+ // The gap between the count and the events above, stated rather than
3529
+ // left to be inferred from a shortfall.
3530
+ toolCallsUndated,
3286
3531
  promptCount: fold.humanPrompts,
3287
3532
  assistantTurns: fold.assistantTurns,
3288
3533
  inputTokens: fold.inputTokens,
@@ -3307,6 +3552,7 @@ async function* extractCursor(snapshotDir, extractionId) {
3307
3552
  if (fold.mode) sessionMetrics.mode = fold.mode;
3308
3553
  if (fold.contextUsagePercent !== null) {
3309
3554
  sessionMetrics.contextUsagePercent = Math.round(fold.contextUsagePercent * 100) / 100;
3555
+ sessionMetrics.contextWindowPeakPct = Math.round(fold.contextUsagePercent * 100) / 1e4;
3310
3556
  }
3311
3557
  const primaryModel = pickTop(fold.models);
3312
3558
  if (primaryModel) sessionMetrics.primaryModel = primaryModel;
@@ -3495,6 +3741,22 @@ async function foldEditDays(historyDir, index) {
3495
3741
  }
3496
3742
  return result;
3497
3743
  }
3744
+ function toolInvocationsOf(request) {
3745
+ const response = request.response;
3746
+ if (!Array.isArray(response)) return [];
3747
+ const calls = [];
3748
+ for (const part of response) {
3749
+ if (typeof part !== "object" || part === null) continue;
3750
+ const entry = part;
3751
+ if (entry.kind !== "toolInvocationSerialized") continue;
3752
+ if (typeof entry.toolCallId !== "string" || entry.toolCallId.length === 0) continue;
3753
+ calls.push({
3754
+ id: entry.toolCallId,
3755
+ name: sanitizeToolName(typeof entry.toolId === "string" ? entry.toolId : void 0)
3756
+ });
3757
+ }
3758
+ return calls;
3759
+ }
3498
3760
  var JSONL_KIND_HEADER = 0;
3499
3761
  var JSONL_KIND_SET = 1;
3500
3762
  var JSONL_KIND_SPLICE = 2;
@@ -3656,7 +3918,8 @@ async function foldChatSessions(workspaceStorageDir) {
3656
3918
  canceledCount: 0,
3657
3919
  errorCount: 0,
3658
3920
  models: /* @__PURE__ */ new Map(),
3659
- requestTimestamps: []
3921
+ requestTimestamps: [],
3922
+ toolCalls: /* @__PURE__ */ new Map()
3660
3923
  };
3661
3924
  for (const r of requests) {
3662
3925
  if (typeof r !== "object" || r === null) continue;
@@ -3675,6 +3938,9 @@ async function foldChatSessions(workspaceStorageDir) {
3675
3938
  if (typeof req.modelId === "string") {
3676
3939
  fold.models.set(req.modelId, (fold.models.get(req.modelId) ?? 0) + 1);
3677
3940
  }
3941
+ for (const call of toolInvocationsOf(req)) {
3942
+ if (!fold.toolCalls.has(call.id)) fold.toolCalls.set(call.id, { at: iso, name: call.name });
3943
+ }
3678
3944
  }
3679
3945
  if (fold.requestCount > 0) result.sessions.push(fold);
3680
3946
  }
@@ -3728,8 +3994,22 @@ async function* extractVsCode(source, extractionId) {
3728
3994
  extractionId
3729
3995
  };
3730
3996
  }
3997
+ for (const call of fold.toolCalls.values()) {
3998
+ yield {
3999
+ occurredAt: call.at,
4000
+ store: "vscode",
4001
+ sourceVersion: String(KNOWN_CHAT_SESSION_VERSION),
4002
+ sessionRef: fold.sessionId,
4003
+ repoRef: fold.workspaceRoot,
4004
+ eventKind: "ai_tool_call_started",
4005
+ metrics: { toolName: call.name },
4006
+ provenance: HISTORICAL_PROVENANCE.derived,
4007
+ extractionId
4008
+ };
4009
+ }
3731
4010
  const durationMs = Date.parse(fold.lastTs) - Date.parse(fold.firstTs);
3732
4011
  const sessionMetrics = {
4012
+ toolCallCount: fold.toolCalls.size,
3733
4013
  requestCount: fold.requestCount,
3734
4014
  afterHoursRequests: fold.afterHoursRequests,
3735
4015
  canceledCount: fold.canceledCount,
@@ -4155,7 +4435,7 @@ function importKeyOf(event, ordinal) {
4155
4435
  [event.store, event.sessionRef ?? "", event.eventKind, event.occurredAt, String(ordinal)].join(
4156
4436
  "|"
4157
4437
  )
4158
- ).digest("hex").slice(0, 16);
4438
+ ).digest("hex");
4159
4439
  }
4160
4440
  var workContextMemo = /* @__PURE__ */ new Map();
4161
4441
  function workContextOf(repoRef) {
@@ -4318,7 +4598,7 @@ import * as fs8 from "node:fs/promises";
4318
4598
  import * as os2 from "node:os";
4319
4599
  import * as path8 from "node:path";
4320
4600
  var APP_BUNDLE_ID = "one.ascenda.ascendaMissionControl";
4321
- var HANDOFF_SCHEMA = 2;
4601
+ var HANDOFF_SCHEMA = 3;
4322
4602
  function handoffDir(home = os2.homedir()) {
4323
4603
  return path8.join(home, "Library", "Containers", APP_BUNDLE_ID, "Data", ".ascenda", "history-import");
4324
4604
  }
@@ -4366,6 +4646,8 @@ function buildHandoff(events, extractionId, generatedAt) {
4366
4646
  // counting it in case upstream starts populating it, and the handoff
4367
4647
  // reports the count only when it is non-zero.
4368
4648
  userModifiedEditCount: Number(event.metrics.userModifiedEditCount ?? 0) > 0 ? Number(event.metrics.userModifiedEditCount) : null,
4649
+ toolCallCount: Number(event.metrics.toolCallCount ?? 0),
4650
+ subagentToolCallCount: Number(event.metrics.subagentToolCallCount ?? 0),
4369
4651
  subagentTranscripts: Number(event.metrics.subagentTranscripts ?? 0),
4370
4652
  days: event.dayBreakdown ?? [],
4371
4653
  provenance: event.provenance
@@ -4411,6 +4693,8 @@ function buildCursorHandoff(events, extractionId, generatedAt) {
4411
4693
  contextUsagePercent: typeof event.metrics.contextUsagePercent === "number" ? event.metrics.contextUsagePercent : null,
4412
4694
  humanChangesCount: Number(event.metrics.humanChangesCount ?? 0),
4413
4695
  approximateLintErrorsCount: Number(event.metrics.approximateLintErrorsCount ?? 0),
4696
+ toolCallCount: Number(event.metrics.toolCallCount ?? 0),
4697
+ toolCallsUndated: Number(event.metrics.toolCallsUndated ?? 0),
4414
4698
  subagentComposers: Number(event.metrics.subagentComposers ?? 0),
4415
4699
  days: event.dayBreakdown ?? [],
4416
4700
  provenance: event.provenance
@@ -4463,6 +4747,7 @@ function buildVsCodeHandoff(events, extractionId, generatedAt) {
4463
4747
  primaryModel: typeof event.metrics.primaryModel === "string" ? event.metrics.primaryModel : null,
4464
4748
  canceledCount: Number(event.metrics.canceledCount ?? 0),
4465
4749
  errorCount: Number(event.metrics.errorCount ?? 0),
4750
+ toolCallCount: Number(event.metrics.toolCallCount ?? 0),
4466
4751
  days: event.dayBreakdown ?? [],
4467
4752
  provenance: event.provenance
4468
4753
  });
@@ -4524,8 +4809,6 @@ async function safeScan(store, rootPath, scan) {
4524
4809
  }
4525
4810
  }
4526
4811
  var READ_FAILURE_METRICS = [
4527
- "unparsedFiles",
4528
- "unreadableFiles",
4529
4812
  "unparsedHistoryFiles",
4530
4813
  "unreadableHistoryFiles",
4531
4814
  "malformedHistoryEntries",
@@ -4534,7 +4817,10 @@ var READ_FAILURE_METRICS = [
4534
4817
  "unrecognisedChatSessionFiles",
4535
4818
  "malformedChatSessionLines",
4536
4819
  "unparsedLines",
4537
- "unparsedTranscripts"
4820
+ // Projects, not files — the total is a count of things this run could not
4821
+ // read, and the warning wording ("file(s)/record(s)") is looser than the
4822
+ // set it sums.
4823
+ "projectsWithNoReadableTranscript"
4538
4824
  ];
4539
4825
  function readFailuresOf(events) {
4540
4826
  let total = 0;
@@ -4547,6 +4833,9 @@ function readFailuresOf(events) {
4547
4833
  }
4548
4834
  return total;
4549
4835
  }
4836
+ function appendAll(target, source) {
4837
+ for (const item of source) target.push(item);
4838
+ }
4550
4839
  async function runSource(outcomes, store, inventory, allEvents, extract, buildStoreHandoff, scanError = null) {
4551
4840
  if (scanError !== null) {
4552
4841
  process.stdout.write(`${inventory.store} FAILED to scan: ${scanError}
@@ -4568,7 +4857,7 @@ async function runSource(outcomes, store, inventory, allEvents, extract, buildSt
4568
4857
  kindCounts[event.eventKind] = (kindCounts[event.eventKind] ?? 0) + 1;
4569
4858
  });
4570
4859
  } catch (error) {
4571
- allEvents.push(...events);
4860
+ appendAll(allEvents, events);
4572
4861
  outcomes.push({
4573
4862
  store,
4574
4863
  status: "failed",
@@ -4581,7 +4870,7 @@ async function runSource(outcomes, store, inventory, allEvents, extract, buildSt
4581
4870
  `);
4582
4871
  return;
4583
4872
  }
4584
- allEvents.push(...events);
4873
+ appendAll(allEvents, events);
4585
4874
  process.stdout.write(`${store} extracted: ${events.length.toLocaleString("en-US")} events
4586
4875
  `);
4587
4876
  for (const [kind, n] of Object.entries(kindCounts)) {