@ascenda-one/history-import 0.1.25 → 0.1.26

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
@@ -152,6 +152,47 @@ line that closes the turn. The rule is in
152
152
  [`src/interruptedRuns.ts`](./src/interruptedRuns.ts), and the desktop app's
153
153
  importer counts by the same one.
154
154
 
155
+ A cut counts in the session you pressed Escape in, and in no other. Resuming
156
+ a session copies the history it inherits, markers included, so before this
157
+ rule every resume in a lineage reported its ancestors' interruptions as
158
+ well — a fifth of the counted total on the store this was measured against.
159
+ The same ownership rule as the minutes (`minutesBasis: owned_lines`).
160
+
161
+ ### When two files both say a line is theirs
162
+
163
+ Most copies keep the session id the line was written with, which is how the
164
+ import tells a copy from the real thing. Some rewrite it to the file the copy
165
+ now sits in, keeping the line's own id and its original timestamp — so the
166
+ test that settles every other copy answers "written here" in both places, and
167
+ both sessions counted the line. Nothing on the line says which of the two
168
+ wrote it: the two records are identical apart from the fields the copy
169
+ rewrote.
170
+
171
+ Where the line sits is what settles it. A line two files claim is contested.
172
+ Inside a file, the run of contested lines at the top is the history it
173
+ inherited, and it ends at the first line that file claims and nobody else
174
+ does — from there on, what it holds it wrote. So a contested line belongs to
175
+ the file still holding it after its own history has started.
176
+
177
+ Not simply the first file the import reaches, which would be the easy rule and
178
+ the wrong one: on the store this was measured against it would hand two fifths
179
+ of these lines to the copy instead, which is the error this whole rule exists
180
+ to remove.
181
+
182
+ **What that leaves.** A lineage whose earliest file the 30-day cleanup has
183
+ already taken has no file that wrote those lines — every one still holding
184
+ them holds them in its inherited head, and nothing left on disk says which
185
+ session did the work. The line is still counted once, by the first file the
186
+ import reaches, the same answer any orphaned line gets. The total is right and
187
+ the session it lands on is a guess. That was 16.1% of contested lines here.
188
+
189
+ Sizes, on the same store: 3.7% of the lines claiming their own file were
190
+ claimed by two of them, and counting each once takes 3.9% off the summed
191
+ active minutes. **Your week doesn't move** — the union of your active time
192
+ holds to a minute, because a line counted twice was one minute counted twice.
193
+ Hands-on shifts a little more than that: a span changes sides when the lines
194
+ around it move, so the unioned hands-on figure moved by 19 minutes in 6,734.
195
+
155
196
  ### What counts as a prompt
156
197
 
157
198
  `promptCount` on a Claude Code session counts the prompts you typed. Claude
@@ -180,13 +221,17 @@ test is `isTypedPromptLine` in
180
221
  Two more rules need the whole store, so the extractor reads every transcript
181
222
  once before it folds any session ([`src/promptLedger.ts`](./src/promptLedger.ts)):
182
223
 
183
- - **Each typed line counts once.** Resuming or forking a session writes a new
184
- transcript that copies the history it inherited, line ids and timestamps
185
- included. The copy is counted by one transcript only: the one the line's
186
- `sessionId` names, or, where that file is gone, the first in the walk's
187
- sorted order. A line with no id is counted wherever it appears. Active and
188
- hands-on minutes aren't deduplicated this way. A resumed session still
189
- carries its ancestors' minutes.
224
+ - **Each line belongs to one session.** Resuming or forking a session writes a
225
+ new transcript that copies the history it inherited, line ids and timestamps
226
+ included. Each copy has one owner: the transcript the line's `sessionId`
227
+ names, or, where the purge took that file, the first in the walk's sorted
228
+ order. The owner counts the prompt, and the owner's timeline carries the
229
+ instant — so `activeMinutes`, the hands-on split, the day slices and
230
+ `startedAt` describe the session you resumed rather than everything behind
231
+ it. Two files can both name themselves on one line, which "When two files
232
+ both say a line is theirs" above settles. A line with no id is counted
233
+ wherever it appears; on the reference store that's `queue-operation` lines
234
+ and nothing else.
190
235
  - **A chip's prompt isn't typed.** A session launched from a `spawn_task` chip
191
236
  opens on the chip's prompt. When a line's text, wrappers stripped, matches a
192
237
  chip that some session in the store offered, it counts in
@@ -202,6 +247,43 @@ typed. A handoff without the stamp counted every `user` line that wasn't a tool
202
247
  result. Prompt counts for the same week run highest under that rule and lowest
203
248
  under `typed_once`. The schema number doesn't move.
204
249
 
250
+ Beside it, `minutesBasis: "owned_lines"` says the minutes were cut the same
251
+ way. `"every_line"` — and an absent stamp — means a transcript's whole
252
+ contents counted there, inherited history included. Gate on this before
253
+ comparing minutes across two handoffs: a store's figures drop when it changes,
254
+ and nothing else in the file says so.
255
+
256
+ ### How much history a resume carries
257
+
258
+ Enough to matter, which is why the rule above covers the minutes and not only
259
+ the counts. On one real store of 984 transcripts, 151 carried inherited lines:
260
+ 275,801 of them, 29% of every dated line in the store. They held the whole of
261
+ the gap between what the sessions claimed and what the clock allowed. Both
262
+ columns are the same 983 sessions, read from one snapshot:
263
+
264
+ | | Every line | Owned lines |
265
+ |---|---|---|
266
+ | Summed active minutes | 70,212 | 53,600 |
267
+ | Summed hands-on minutes | 12,238 | 8,006 |
268
+ | Union of every active interval | 25,078 | 25,077 |
269
+ | Exact-duplicate spans | 262,500 of 873,599 | 21,982 of 635,126 |
270
+ | Sessions apparently spanning over a day | 158 | 154 |
271
+
272
+ The union is the control. It holds to the minute, so no work anyone did was
273
+ dropped: what went was the copy of it. Duplicate spans fell from 30% of the
274
+ store to 3.5%, which matters to any count of how many agents were running at
275
+ once, since a duplicate reads as a second agent. 127 sessions now start when
276
+ they were resumed, 13 of them more than an hour later than they used to.
277
+
278
+ That 3.5% was not the floor, and it was not coincidence: four fifths of it was
279
+ the copy path settled above. On a later and slightly larger snapshot of the
280
+ same store, the duplicate spans go from 21,982 to 4,087 once contested lines
281
+ are settled.
282
+
283
+ An earlier reading of this exposure put it at 0.72% of sessions. It counted
284
+ repeated `sessionRef`s, and a resumed transcript carries a fresh one, so it saw
285
+ three sessions where 151 transcripts were affected.
286
+
205
287
  ### Autonomy bands
206
288
 
207
289
  `permissionMode` is on the transcript's human-prompt lines and nowhere else —
package/dist/cli.js CHANGED
@@ -540,6 +540,10 @@ var require_metricKeys = __commonJS({
540
540
  orphanedBubbles: { readBy: ["diagnostic"] },
541
541
  orphanedSubagentBubbles: { readBy: ["diagnostic"] },
542
542
  sessionsWithoutTimeline: { readBy: ["diagnostic"] },
543
+ sessionsWithOnlyInheritedLines: {
544
+ readBy: ["diagnostic"],
545
+ note: "Claude Code transcripts holding nothing but a copy of an ancestor's history, so no line's instant is theirs and no session is emitted for them. Excluded by rule, not a read failure: the work is in the transcript that owns it."
546
+ },
543
547
  emptyComposers: { readBy: ["diagnostic"] },
544
548
  apiErrorCount: { readBy: ["diagnostic"] },
545
549
  assistantTurns: { readBy: ["diagnostic"] },
@@ -735,6 +739,16 @@ var require_out = __commonJS({
735
739
  // `ai_prompt_submitted` closes. Neutral so a marker sent once per turn
736
740
  // doesn't weigh on any workload leg.
737
741
  ai_turn_completed: "neutral",
742
+ // The agent stopped and is waiting on the person: Claude Code's
743
+ // `Notification`, Codex's `PermissionRequest`. Supervision, beside the other
744
+ // oversight overhead, because that is what it is — time the person spends
745
+ // attending to the agent rather than to the work.
746
+ //
747
+ // Deliberately NOT risk. Risk is the leg that reads as something going
748
+ // wrong, and being asked is not a failure; classifying it there is the first
749
+ // step toward an interruption count becoming a score, a streak, or something
750
+ // that fires on a miss. It must never become any of those.
751
+ supervision_interruption: "supervision",
738
752
  // Semantic (agent-observed) — see SEMANTIC_WORK_SIGNAL_EVENT_TYPES.
739
753
  approach_churn_detected: "risk",
740
754
  goal_drift_detected: "risk",
@@ -4385,7 +4399,6 @@ function localDayCounts(timestamps) {
4385
4399
  }
4386
4400
  function sliceSessionByLocalDay(timestamps, options = {}) {
4387
4401
  const instants = timestamps.filter((t) => typeof t === "string" && t.length > 0).map((t) => new Date(t)).filter((d) => Number.isFinite(d.getTime())).sort((a, b) => a.getTime() - b.getTime());
4388
- if (instants.length === 0) return [];
4389
4402
  const prompts = /* @__PURE__ */ new Map();
4390
4403
  for (const t of instants) {
4391
4404
  const key = localDayKey(t);
@@ -4583,14 +4596,109 @@ var PromptLedger = class {
4583
4596
  this.owners.set(uuid, null);
4584
4597
  return true;
4585
4598
  }
4599
+ /**
4600
+ * Hands a contested line to the transcript that wrote it.
4601
+ *
4602
+ * The ledger picks an owner by `sessionId` too, so a copy that rewrote it
4603
+ * reads as home here as well. That never counted a prompt twice — the id is
4604
+ * the key either way — but it could count it in the wrong session, whichever
4605
+ * of the two the walk reached first. `HomeClaims` settles the same question
4606
+ * for the timeline, and one answer serves both: a prompt and the instant it
4607
+ * happened at cannot belong to different sessions.
4608
+ */
4609
+ applyContestedOwners(owners) {
4610
+ for (const [uuid, transcript] of owners) {
4611
+ if (!this.owners.has(uuid)) continue;
4612
+ this.owners.set(uuid, { transcript, home: true });
4613
+ }
4614
+ }
4586
4615
  /** Distinct chip prompts seen, for tests and diagnostics. */
4587
4616
  get chipPromptCount() {
4588
4617
  return this.chipPrompts.size;
4589
4618
  }
4590
4619
  };
4620
+ var HomeClaims = class {
4621
+ /** Id to the first transcript that claimed it, in walk order. */
4622
+ firstClaim = /* @__PURE__ */ new Map();
4623
+ /** Id to every transcript claiming it, once a second one does. */
4624
+ contested = /* @__PURE__ */ new Map();
4625
+ /** Takes one line of the pre-read, in walk order. */
4626
+ observe(uuid, sessionId, transcript) {
4627
+ if (typeof uuid !== "string" || !isHomeTranscript(transcript, sessionId)) return;
4628
+ const first = this.firstClaim.get(uuid);
4629
+ if (first === void 0) {
4630
+ this.firstClaim.set(uuid, transcript);
4631
+ return;
4632
+ }
4633
+ if (first === transcript) return;
4634
+ const all = this.contested.get(uuid) ?? /* @__PURE__ */ new Set([first]);
4635
+ all.add(transcript);
4636
+ this.contested.set(uuid, all);
4637
+ }
4638
+ get contestedCount() {
4639
+ return this.contested.size;
4640
+ }
4641
+ /** Every transcript holding a contested id — the only ones worth re-reading. */
4642
+ transcriptsToClassify() {
4643
+ const out = /* @__PURE__ */ new Set();
4644
+ for (const files of this.contested.values()) for (const f of files) out.add(f);
4645
+ return [...out];
4646
+ }
4647
+ has(uuid) {
4648
+ return this.contested.has(uuid);
4649
+ }
4650
+ /** Every contested id, so each one can be given an owner. */
4651
+ contestedUuids() {
4652
+ return this.contested.keys();
4653
+ }
4654
+ /** The first transcript in walk order that claimed this id. */
4655
+ firstClaimOf(uuid) {
4656
+ return this.firstClaim.get(uuid);
4657
+ }
4658
+ };
4659
+ async function resolveContestedOwners(claims, walkOrder) {
4660
+ const owners = /* @__PURE__ */ new Map();
4661
+ const rank = new Map(walkOrder.map((file, i) => [file, i]));
4662
+ const toRead = claims.transcriptsToClassify().sort((a, b) => (rank.get(a) ?? 0) - (rank.get(b) ?? 0));
4663
+ for (const transcript of toRead) {
4664
+ let headOver = false;
4665
+ const own = [];
4666
+ try {
4667
+ await eachLine(transcript, (line) => {
4668
+ if (!line.includes('"uuid"')) return;
4669
+ let parsed;
4670
+ try {
4671
+ parsed = JSON.parse(line);
4672
+ } catch {
4673
+ return;
4674
+ }
4675
+ if (typeof parsed !== "object" || parsed === null) return;
4676
+ const record = parsed;
4677
+ if (typeof record.uuid !== "string") return;
4678
+ if (!isHomeTranscript(transcript, record.sessionId)) return;
4679
+ if (!claims.has(record.uuid)) {
4680
+ headOver = true;
4681
+ return;
4682
+ }
4683
+ if (headOver) own.push(record.uuid);
4684
+ });
4685
+ } catch {
4686
+ }
4687
+ for (const uuid of own) if (!owners.has(uuid)) owners.set(uuid, transcript);
4688
+ }
4689
+ for (const uuid of claims.contestedUuids()) {
4690
+ if (owners.has(uuid)) continue;
4691
+ const first = claims.firstClaimOf(uuid);
4692
+ if (first !== void 0) owners.set(uuid, first);
4693
+ }
4694
+ return owners;
4695
+ }
4591
4696
  function mayMatter(line) {
4592
4697
  return line.includes('"user"') || line.includes(SPAWN_TASK_TOOL_NAME);
4593
4698
  }
4699
+ function mayClaim(line) {
4700
+ return line.includes('"uuid"');
4701
+ }
4594
4702
  async function eachLine(transcript, onLine) {
4595
4703
  const stream = createReadStream(transcript, { encoding: "utf8", highWaterMark: 1 << 20 });
4596
4704
  let pending = [];
@@ -4609,10 +4717,12 @@ async function eachLine(transcript, onLine) {
4609
4717
  }
4610
4718
  async function readPromptLedger(mainTranscripts, subagentTranscripts, isToolResult) {
4611
4719
  const ledger = new PromptLedger();
4720
+ const claims = new HomeClaims();
4612
4721
  const read = async (transcript, isSidechain) => {
4613
4722
  try {
4614
4723
  await eachLine(transcript, (line) => {
4615
- if (!mayMatter(line)) return;
4724
+ const claiming = !isSidechain && mayClaim(line);
4725
+ if (!claiming && !mayMatter(line)) return;
4616
4726
  let parsed;
4617
4727
  try {
4618
4728
  parsed = JSON.parse(line);
@@ -4620,15 +4730,59 @@ async function readPromptLedger(mainTranscripts, subagentTranscripts, isToolResu
4620
4730
  return;
4621
4731
  }
4622
4732
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return;
4623
- ledger.observe(parsed, transcript, { isSidechain, isToolResult });
4733
+ const record = parsed;
4734
+ if (claiming) claims.observe(record.uuid, record.sessionId, transcript);
4735
+ if (mayMatter(line)) {
4736
+ ledger.observe(record, transcript, { isSidechain, isToolResult });
4737
+ }
4624
4738
  });
4625
4739
  } catch {
4626
4740
  }
4627
4741
  };
4628
4742
  for (const transcript of mainTranscripts) await read(transcript, false);
4629
4743
  for (const transcript of subagentTranscripts) await read(transcript, true);
4630
- return ledger;
4744
+ return { ledger, claims };
4631
4745
  }
4746
+ var TimelineOwnership = class {
4747
+ /** Basenames of every main-thread transcript the walk listed. */
4748
+ transcriptIds;
4749
+ /** Ids of inherited lines whose home file is gone, claimed as they're met. */
4750
+ claimedOrphans = /* @__PURE__ */ new Set();
4751
+ /**
4752
+ * Ids two transcripts both claim as written in them, mapped to the one that
4753
+ * wrote it — see `HomeClaims`. Empty where no copy rewrote a `sessionId`,
4754
+ * which is most stores and every store this rule's first version was
4755
+ * measured on.
4756
+ */
4757
+ contestedOwners;
4758
+ constructor(mainTranscripts, contestedOwners = /* @__PURE__ */ new Map()) {
4759
+ this.transcriptIds = new Set(mainTranscripts.map((file) => path4.basename(file, ".jsonl")));
4760
+ this.contestedOwners = contestedOwners;
4761
+ }
4762
+ /**
4763
+ * Whether this transcript's fold counts this line's instant. Called once per
4764
+ * known line, in the extractor's walk order — the orphan rule is first-come,
4765
+ * so a different order would hand a purged lineage's minutes to a different
4766
+ * fold.
4767
+ */
4768
+ owns(line, transcript, opts) {
4769
+ if (opts.isSidechain) return true;
4770
+ if (line.sessionId === null || line.uuid === null) return true;
4771
+ if (isHomeTranscript(transcript, line.sessionId)) {
4772
+ const owner = this.contestedOwners.get(line.uuid);
4773
+ if (owner === void 0) return true;
4774
+ return owner === transcript;
4775
+ }
4776
+ if (this.transcriptIds.has(line.sessionId)) return false;
4777
+ if (this.claimedOrphans.has(line.uuid)) return false;
4778
+ this.claimedOrphans.add(line.uuid);
4779
+ return true;
4780
+ }
4781
+ /** Ids held for orphaned lines, for tests and diagnostics. */
4782
+ get orphanLineCount() {
4783
+ return this.claimedOrphans.size;
4784
+ }
4785
+ };
4632
4786
 
4633
4787
  // src/extractors/claudeCode.ts
4634
4788
  var KNOWN_CLAUDE_LINE_TYPES = [
@@ -4676,6 +4830,7 @@ function sniffClaudeLine(line) {
4676
4830
  kind: type,
4677
4831
  sourceVersion: typeof record.version === "string" ? record.version : null,
4678
4832
  occurredAt: typeof record.timestamp === "string" ? record.timestamp : null,
4833
+ uuid: typeof record.uuid === "string" ? record.uuid : null,
4679
4834
  sessionId: typeof record.sessionId === "string" ? record.sessionId : null,
4680
4835
  model: message && typeof message.model === "string" ? message.model : null
4681
4836
  };
@@ -4775,6 +4930,7 @@ function newFold(sessionId, projectSlug) {
4775
4930
  toolCalls: [],
4776
4931
  timelinePoints: [],
4777
4932
  undatedTimelineLines: 0,
4933
+ inheritedTimelineLines: 0,
4778
4934
  humanPromptTimestamps: [],
4779
4935
  interruptedRuns: 0,
4780
4936
  interruptedRunTimestamps: []
@@ -4834,11 +4990,12 @@ async function foldLinesInto(fold, filePath, opts) {
4834
4990
  fold.unknownLines += 1;
4835
4991
  continue;
4836
4992
  }
4837
- if (!opts.isSidechain && sniffed.sessionId && fold.sessionId !== sniffed.sessionId) {
4993
+ const owned = opts.ownership.owns(sniffed, filePath, { isSidechain: opts.isSidechain });
4994
+ if (owned && !opts.isSidechain && sniffed.sessionId && fold.sessionId !== sniffed.sessionId) {
4838
4995
  fold.sessionId = sniffed.sessionId;
4839
4996
  }
4840
4997
  if (sniffed.sourceVersion) fold.sourceVersion = sniffed.sourceVersion;
4841
- if (sniffed.occurredAt) {
4998
+ if (owned && sniffed.occurredAt) {
4842
4999
  if (!fold.firstTs || sniffed.occurredAt < fold.firstTs) fold.firstTs = sniffed.occurredAt;
4843
5000
  if (!fold.lastTs || sniffed.occurredAt > fold.lastTs) fold.lastTs = sniffed.occurredAt;
4844
5001
  }
@@ -4855,7 +5012,7 @@ async function foldLinesInto(fold, filePath, opts) {
4855
5012
  if (!opts.isSidechain) {
4856
5013
  const step = stepRun(turnRunning, record, isToolResultUserLine);
4857
5014
  turnRunning = step.running;
4858
- if (step.cut) {
5015
+ if (step.cut && owned) {
4859
5016
  fold.interruptedRuns += 1;
4860
5017
  fold.interruptedRunTimestamps.push(sniffed.occurredAt);
4861
5018
  }
@@ -4966,7 +5123,9 @@ async function foldLinesInto(fold, filePath, opts) {
4966
5123
  default:
4967
5124
  break;
4968
5125
  }
4969
- if (sniffed.occurredAt) {
5126
+ if (sniffed.occurredAt && !owned) {
5127
+ fold.inheritedTimelineLines += 1;
5128
+ } else if (sniffed.occurredAt) {
4970
5129
  const ms = Date.parse(sniffed.occurredAt);
4971
5130
  if (Number.isFinite(ms)) {
4972
5131
  fold.timelinePoints.push({
@@ -4984,9 +5143,9 @@ async function foldLinesInto(fold, filePath, opts) {
4984
5143
  await handle.close();
4985
5144
  }
4986
5145
  }
4987
- async function foldTranscript(filePath, projectSlug, ledger) {
5146
+ async function foldTranscript(filePath, projectSlug, ledger, ownership) {
4988
5147
  const fold = newFold(path5.basename(filePath, ".jsonl"), projectSlug);
4989
- await foldLinesInto(fold, filePath, { isSidechain: false, ledger });
5148
+ await foldLinesInto(fold, filePath, { isSidechain: false, ledger, ownership });
4990
5149
  return fold;
4991
5150
  }
4992
5151
  async function walkJsonlFiles(root) {
@@ -5036,6 +5195,7 @@ async function* extractClaudeCode(snapshotDir, extractionId) {
5036
5195
  let windowNewest = null;
5037
5196
  let sessionCount = 0;
5038
5197
  let projectsWithNoReadableTranscript = 0;
5198
+ let sessionsWithOnlyInheritedLines = 0;
5039
5199
  const plan = [];
5040
5200
  for (const slug of projectSlugs.sort()) {
5041
5201
  const dir = path5.join(projectsDir, slug);
@@ -5059,17 +5219,21 @@ async function* extractClaudeCode(snapshotDir, extractionId) {
5059
5219
  sessionDirs
5060
5220
  });
5061
5221
  }
5062
- const ledger = await readPromptLedger(
5063
- plan.flatMap((p) => p.topLevelFiles.map((name) => path5.join(p.dir, name))),
5222
+ const mainTranscripts = plan.flatMap((p) => p.topLevelFiles.map((name) => path5.join(p.dir, name)));
5223
+ const { ledger, claims } = await readPromptLedger(
5224
+ mainTranscripts,
5064
5225
  plan.flatMap((p) => p.sessionDirs.flatMap((d) => d.files)),
5065
5226
  isToolResultUserLine
5066
5227
  );
5228
+ const contestedOwners = await resolveContestedOwners(claims, mainTranscripts);
5229
+ ledger.applyContestedOwners(contestedOwners);
5230
+ const ownership = new TimelineOwnership(mainTranscripts, contestedOwners);
5067
5231
  for (const { slug, dir, topLevelFiles, otherFiles, sessionDirs } of plan) {
5068
5232
  let slugJsonlFiles = topLevelFiles.length;
5069
5233
  let slugOtherFiles = otherFiles;
5070
5234
  const foldsByKey = /* @__PURE__ */ new Map();
5071
5235
  for (const name of topLevelFiles) {
5072
- const fold = await foldTranscript(path5.join(dir, name), slug, ledger);
5236
+ const fold = await foldTranscript(path5.join(dir, name), slug, ledger, ownership);
5073
5237
  foldsByKey.set(path5.basename(name, ".jsonl"), fold);
5074
5238
  }
5075
5239
  for (const { name: sessionDirName, files: nestedFiles, otherFiles: nestedOther } of sessionDirs) {
@@ -5083,12 +5247,15 @@ async function* extractClaudeCode(snapshotDir, extractionId) {
5083
5247
  }
5084
5248
  for (const nestedPath of nestedFiles) {
5085
5249
  fold.subagentTranscripts += 1;
5086
- await foldLinesInto(fold, nestedPath, { isSidechain: true, ledger });
5250
+ await foldLinesInto(fold, nestedPath, { isSidechain: true, ledger, ownership });
5087
5251
  }
5088
5252
  }
5089
5253
  if (slugJsonlFiles === 0 && slugOtherFiles > 0) projectsWithNoReadableTranscript += 1;
5090
5254
  for (const fold of foldsByKey.values()) {
5091
- if (!fold.firstTs || !fold.lastTs) continue;
5255
+ if (!fold.firstTs || !fold.lastTs) {
5256
+ if (fold.inheritedTimelineLines > 0) sessionsWithOnlyInheritedLines += 1;
5257
+ continue;
5258
+ }
5092
5259
  sessionCount += 1;
5093
5260
  if (!windowOldest || fold.firstTs < windowOldest) windowOldest = fold.firstTs;
5094
5261
  if (!windowNewest || fold.lastTs > windowNewest) windowNewest = fold.lastTs;
@@ -5265,7 +5432,7 @@ async function* extractClaudeCode(snapshotDir, extractionId) {
5265
5432
  }
5266
5433
  }
5267
5434
  }
5268
- if (windowOldest && windowNewest || projectsWithNoReadableTranscript > 0) {
5435
+ if (windowOldest && windowNewest || projectsWithNoReadableTranscript > 0 || sessionsWithOnlyInheritedLines > 0) {
5269
5436
  const window = windowOldest && windowNewest ? { windowOldest, windowNewest } : {};
5270
5437
  yield {
5271
5438
  // No window means nothing datable was read; the only honest timestamp
@@ -5279,7 +5446,8 @@ async function* extractClaudeCode(snapshotDir, extractionId) {
5279
5446
  metrics: {
5280
5447
  ...window,
5281
5448
  sessionCount,
5282
- projectsWithNoReadableTranscript
5449
+ projectsWithNoReadableTranscript,
5450
+ sessionsWithOnlyInheritedLines
5283
5451
  },
5284
5452
  provenance: HISTORICAL_PROVENANCE.derived,
5285
5453
  extractionId
@@ -7688,6 +7856,10 @@ function buildHandoff(events, extractionId, generatedAt) {
7688
7856
  // and the prompts chips write, and counts each typed line in one session
7689
7857
  // only, so `promptCount` below is typed prompts, once each.
7690
7858
  promptBasis: "typed_once",
7859
+ // And the same lineage rule on the timeline: an inherited line's instant
7860
+ // belongs to the session it was written in, so the minutes below are this
7861
+ // session's own and `startedAt` is when it was resumed.
7862
+ minutesBasis: "owned_lines",
7691
7863
  // And what each of those figures measures, keyed by the path a reader
7692
7864
  // walks. The `projects` rollup below is why this store needs the stamp
7693
7865
  // most: it carries the summed pair and the unioned pair under one
package/dist/daySlice.js CHANGED
@@ -97,8 +97,21 @@ export function sliceSessionByLocalDay(timestamps, options = {}) {
97
97
  .map((t) => new Date(t))
98
98
  .filter((d) => Number.isFinite(d.getTime()))
99
99
  .sort((a, b) => a.getTime() - b.getTime());
100
- if (instants.length === 0)
101
- return [];
100
+ // No early return for an empty prompt list. There used to be one, and it
101
+ // ran before `activeInstants` and `interruptedRunTimestamps` were read, so a
102
+ // session with active time but no prompt of its own got no slices at all —
103
+ // silently, since the session's own `activeMinutes` were fine and only the
104
+ // placement of them vanished. Zero-prompt sessions are ordinary: one opened
105
+ // from a chip, or a resume whose prompts all belong to an ancestor. On the
106
+ // reference store 103 of 987 sessions have no prompt of their own, 97 of
107
+ // them hold active time, and 1,523 minutes were dropped from the day
108
+ // breakdown against 52,267 placed. Six cut-short runs went with them, which
109
+ // the option above promises a slice for "even with no prompt on it".
110
+ //
111
+ // Nothing replaces the guard, because nothing needs to: with no prompts, no
112
+ // instants and no cuts, `days` below is empty and the map returns `[]` on
113
+ // its own. "Nothing to place" is still an empty array, and it is now the
114
+ // answer to the whole question rather than to the first part of it.
102
115
  const prompts = new Map();
103
116
  for (const t of instants) {
104
117
  const key = localDayKey(t);
@@ -1 +1 @@
1
- {"version":3,"file":"daySlice.js","sourceRoot":"","sources":["../src/daySlice.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"daySlice.js","sourceRoot":"","sources":["../src/daySlice.ts"],"names":[],"mappings":"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"}