@yhong91/vibetime 0.1.72 → 0.1.74

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.
Files changed (2) hide show
  1. package/bin/vibetime.mjs +284 -68
  2. package/package.json +1 -1
package/bin/vibetime.mjs CHANGED
@@ -2047,7 +2047,7 @@ function claudeStyleFileMetrics(tool, input) {
2047
2047
  }
2048
2048
 
2049
2049
  // src/lib/constants.ts
2050
- var PACKAGE_VERSION = true ? "0.1.72" : "0.1.1";
2050
+ var PACKAGE_VERSION = true ? "0.1.74" : "0.1.1";
2051
2051
  var GENERATED_MARKER = "Generated by vibetime.";
2052
2052
  var DEFAULT_API_URL = "http://121.196.224.82:3001";
2053
2053
  var DEFAULT_BACKFILL_BATCH_SIZE = 50;
@@ -5163,7 +5163,6 @@ async function parseCodexSessionFile(filePath, options) {
5163
5163
  let lastTurnIdForComplete;
5164
5164
  let turnIdAtLastUserMessage;
5165
5165
  let sessionMetaLocked = false;
5166
- let forkedSession = false;
5167
5166
  let lastTotalTokenUsage;
5168
5167
  let lastPerCallUsageKey;
5169
5168
  let compactionPending = false;
@@ -5184,7 +5183,6 @@ async function parseCodexSessionFile(filePath, options) {
5184
5183
  }
5185
5184
  sessionMetaLocked = true;
5186
5185
  const forkedFromId = stringField(payload, "forked_from_id");
5187
- forkedSession = Boolean(forkedFromId);
5188
5186
  const inherited = forkedFromId ? await readPersistedSessionContextFromOptions(options, forkedFromId) : void 0;
5189
5187
  sessionId = stringField(payload, "id") || sessionId;
5190
5188
  cwd = inherited?.cwd || stringField(payload, "cwd") || cwd;
@@ -5336,7 +5334,7 @@ async function parseCodexSessionFile(filePath, options) {
5336
5334
  project,
5337
5335
  model: rewriteCodexModelForTier(model, serviceTier),
5338
5336
  confidence: "partial",
5339
- metrics: excludeForkInheritedCache(usage, forkedSession)
5337
+ metrics: usage
5340
5338
  }), { filePath, sourcePathHash, lineNumber, topType, payloadType, options }));
5341
5339
  lastPerCallUsageKey = usageKey;
5342
5340
  }
@@ -5354,7 +5352,7 @@ async function parseCodexSessionFile(filePath, options) {
5354
5352
  project,
5355
5353
  model: rewriteCodexModelForTier(model, serviceTier),
5356
5354
  confidence: "partial",
5357
- metrics: excludeForkInheritedCache({ ...lastUsage, reasoningEffort }, forkedSession)
5355
+ metrics: { ...lastUsage, reasoningEffort }
5358
5356
  }), { filePath, sourcePathHash, lineNumber, topType, payloadType, options }));
5359
5357
  }
5360
5358
  lastTotalTokenUsage = { input, cached, output, reasoning, total };
@@ -5378,7 +5376,7 @@ async function parseCodexSessionFile(filePath, options) {
5378
5376
  project,
5379
5377
  model: rewriteCodexModelForTier(model, serviceTier),
5380
5378
  confidence: "partial",
5381
- metrics: excludeForkInheritedCache({
5379
+ metrics: {
5382
5380
  tokensInput: deltaInput > 0 ? deltaInput : void 0,
5383
5381
  tokensCachedInput: deltaCached > 0 ? deltaCached : void 0,
5384
5382
  tokensOutput: deltaOutput > 0 ? deltaOutput : void 0,
@@ -5386,7 +5384,7 @@ async function parseCodexSessionFile(filePath, options) {
5386
5384
  tokensTotal: deltaTotal > 0 ? deltaTotal : void 0,
5387
5385
  modelContextWindow: usage.modelContextWindow,
5388
5386
  reasoningEffort
5389
- }, forkedSession)
5387
+ }
5390
5388
  }), { filePath, sourcePathHash, lineNumber, topType, payloadType, options }));
5391
5389
  }
5392
5390
  } else {
@@ -5399,7 +5397,7 @@ async function parseCodexSessionFile(filePath, options) {
5399
5397
  project,
5400
5398
  model: rewriteCodexModelForTier(model, serviceTier),
5401
5399
  confidence: "partial",
5402
- metrics: excludeForkInheritedCache({
5400
+ metrics: {
5403
5401
  tokensInput: input,
5404
5402
  tokensCachedInput: cached,
5405
5403
  tokensOutput: output,
@@ -5407,7 +5405,7 @@ async function parseCodexSessionFile(filePath, options) {
5407
5405
  tokensTotal: total,
5408
5406
  modelContextWindow: usage.modelContextWindow,
5409
5407
  reasoningEffort
5410
- }, forkedSession)
5408
+ }
5411
5409
  }), { filePath, sourcePathHash, lineNumber, topType, payloadType, options }));
5412
5410
  }
5413
5411
  lastTotalTokenUsage = { input, cached, output, reasoning, total };
@@ -5708,18 +5706,6 @@ function tokenUsageFromRecord(usage, info) {
5708
5706
  modelContextWindow: numberField(info, "model_context_window")
5709
5707
  };
5710
5708
  }
5711
- function excludeForkInheritedCache(usage, forked) {
5712
- if (!forked || !usage) {
5713
- return usage;
5714
- }
5715
- const cached = usage.tokensCachedInput ?? 0;
5716
- return {
5717
- ...usage,
5718
- tokensInput: Math.max(0, (usage.tokensInput ?? 0) - cached) || void 0,
5719
- tokensCachedInput: void 0,
5720
- tokensTotal: Math.max(0, (usage.tokensTotal ?? 0) - cached) || void 0
5721
- };
5722
- }
5723
5709
  function toolNameFromPayload(payload, payloadType) {
5724
5710
  if (payloadType === "web_search_call") {
5725
5711
  return "web_search";
@@ -9079,11 +9065,121 @@ function projectFromCwd(cwd, repoRoot) {
9079
9065
  const basis = repoRoot || cwd;
9080
9066
  return basis ? path15.basename(basis) || SOURCE : SOURCE;
9081
9067
  }
9082
- function isoFromUnix(value) {
9068
+ function unixFrom(value) {
9083
9069
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
9084
9070
  return void 0;
9085
9071
  }
9086
- return timestampFrom(value);
9072
+ return value;
9073
+ }
9074
+ function isoFromUnix(value) {
9075
+ const unix = unixFrom(value);
9076
+ return unix === void 0 ? void 0 : timestampFrom(unix);
9077
+ }
9078
+ function maxUnix(...values) {
9079
+ let max;
9080
+ for (const value of values) {
9081
+ const unix = unixFrom(value);
9082
+ if (unix === void 0) {
9083
+ continue;
9084
+ }
9085
+ if (max === void 0 || unix > max) {
9086
+ max = unix;
9087
+ }
9088
+ }
9089
+ return max;
9090
+ }
9091
+ function isReapEndReason(reason) {
9092
+ return reason === "ws_orphan_reap" || reason === "startup_orphan_reap";
9093
+ }
9094
+ function isSyntheticUserContent(content) {
9095
+ if (!content) {
9096
+ return false;
9097
+ }
9098
+ const trimmed = content.trim();
9099
+ return trimmed.startsWith("[CONTEXT COMPACTION") || trimmed.startsWith("[IMPORTANT: Background process");
9100
+ }
9101
+ var INT_METRIC_KEYS = [
9102
+ "tokensInput",
9103
+ "tokensOutput",
9104
+ "tokensCacheReadInput",
9105
+ "tokensCacheCreationInput",
9106
+ "tokensCachedInput",
9107
+ "tokensReasoningOutput",
9108
+ "tokensTotal",
9109
+ "modelCalls"
9110
+ ];
9111
+ function splitMetricBag(bag, fractions) {
9112
+ if (fractions.length === 0) {
9113
+ return [];
9114
+ }
9115
+ if (fractions.length === 1) {
9116
+ return [{ ...bag }];
9117
+ }
9118
+ const out = fractions.map(() => ({}));
9119
+ for (const key of INT_METRIC_KEYS) {
9120
+ const value = bag[key];
9121
+ if (typeof value !== "number" || !Number.isFinite(value) || value === 0) {
9122
+ continue;
9123
+ }
9124
+ let allocated = 0;
9125
+ for (let i = 0; i < fractions.length; i += 1) {
9126
+ const part = i === fractions.length - 1 ? value - allocated : Math.floor(value * fractions[i]);
9127
+ allocated += part;
9128
+ if (part) {
9129
+ out[i][key] = part;
9130
+ }
9131
+ }
9132
+ }
9133
+ const cost = bag.costUsd;
9134
+ if (typeof cost === "number" && Number.isFinite(cost) && cost > 0) {
9135
+ let allocated = 0;
9136
+ for (let i = 0; i < fractions.length; i += 1) {
9137
+ const part = i === fractions.length - 1 ? Math.round((cost - allocated) * 1e6) / 1e6 : Math.round(cost * fractions[i] * 1e6) / 1e6;
9138
+ allocated += part;
9139
+ if (part) {
9140
+ out[i].costUsd = part;
9141
+ }
9142
+ }
9143
+ }
9144
+ return out;
9145
+ }
9146
+ function metricBagHasWork(bag) {
9147
+ return INT_METRIC_KEYS.some((key) => (bag[key] || 0) > 0);
9148
+ }
9149
+ function allocateUsageAcrossTurns(usage, turns) {
9150
+ const first = unixFrom(usage.first_seen);
9151
+ const last = unixFrom(usage.last_seen);
9152
+ const start = first ?? last;
9153
+ const end = last ?? first;
9154
+ if (start === void 0 || end === void 0) {
9155
+ const turn = turns.at(-1);
9156
+ return [{ turn, fraction: 1, tsUnix: turn?.endUnix || turn?.startedUnix || 0 }];
9157
+ }
9158
+ if (turns.length === 0) {
9159
+ return [{ fraction: 1, tsUnix: end }];
9160
+ }
9161
+ if (end <= start) {
9162
+ const turn = turns.find((item) => start >= item.startedUnix && start < item.endUnix) || [...turns].reverse().find((item) => item.startedUnix <= start) || turns[0];
9163
+ return [{ turn, fraction: 1, tsUnix: start }];
9164
+ }
9165
+ const overlaps = [];
9166
+ for (const turn of turns) {
9167
+ const lo = Math.max(start, turn.startedUnix);
9168
+ const hi = Math.min(end, turn.endUnix);
9169
+ if (hi > lo) {
9170
+ overlaps.push({ turn, overlap: hi - lo });
9171
+ }
9172
+ }
9173
+ const total = overlaps.reduce((sum, item) => sum + item.overlap, 0);
9174
+ if (total <= 0) {
9175
+ const turn = [...turns].reverse().find((item) => item.startedUnix <= end) || turns[0];
9176
+ return [{ turn, fraction: 1, tsUnix: Math.min(end, turn?.endUnix || end) }];
9177
+ }
9178
+ return overlaps.map((item) => ({
9179
+ turn: item.turn,
9180
+ fraction: item.overlap / total,
9181
+ tsUnix: Math.min(end, item.turn.endUnix)
9182
+ }));
9087
9183
  }
9088
9184
  function tableColumns(db, table) {
9089
9185
  try {
@@ -9260,6 +9356,7 @@ async function parseHermesDb(filePath, options) {
9260
9356
  "parent_session_id",
9261
9357
  "started_at",
9262
9358
  "ended_at",
9359
+ "end_reason",
9263
9360
  "message_count",
9264
9361
  "tool_call_count",
9265
9362
  "input_tokens",
@@ -9349,8 +9446,9 @@ async function parseHermesDb(filePath, options) {
9349
9446
  if (!sessionHasWork(session, usageRows) && messages.length === 0) {
9350
9447
  continue;
9351
9448
  }
9352
- const startedTs = isoFromUnix(session.started_at);
9353
- if (!startedTs) {
9449
+ const startedUnix = unixFrom(session.started_at);
9450
+ const startedTs = isoFromUnix(startedUnix);
9451
+ if (!startedUnix || !startedTs) {
9354
9452
  continue;
9355
9453
  }
9356
9454
  lineNumber += 1;
@@ -9378,7 +9476,15 @@ async function parseHermesDb(filePath, options) {
9378
9476
  if (started) {
9379
9477
  events.push(started);
9380
9478
  }
9381
- const endedTs = isoFromUnix(session.ended_at) || isoFromUnix(session.last_activity_at);
9479
+ const lastMessageUnix = maxUnix(...messages.map((message) => message.timestamp));
9480
+ const lastUsageUnix = maxUnix(
9481
+ ...usageRows.flatMap((usage) => [usage.last_seen, usage.first_seen])
9482
+ );
9483
+ const lastActivityUnix = unixFrom(session.last_activity_at);
9484
+ const endReason = stringField(session, "end_reason");
9485
+ const closedUnix = isReapEndReason(endReason) ? void 0 : unixFrom(session.ended_at);
9486
+ const endedUnix = maxUnix(lastMessageUnix, lastUsageUnix, closedUnix) ?? lastActivityUnix;
9487
+ const endedTs = isoFromUnix(endedUnix);
9382
9488
  if (endedTs && endedTs !== startedTs) {
9383
9489
  lineNumber += 1;
9384
9490
  const ended = makeEvent({
@@ -9398,16 +9504,102 @@ async function parseHermesDb(filePath, options) {
9398
9504
  refs: refs({
9399
9505
  sourceId: `${ctx.rawId}:session:ended`,
9400
9506
  hermesPlatform: ctx.platform,
9401
- hermesProfile: ctx.profileName
9507
+ hermesProfile: ctx.profileName,
9508
+ hermesEndReason: endReason
9402
9509
  })
9403
9510
  }, lineNumber, filePath, options);
9404
9511
  if (ended) {
9405
9512
  events.push(ended);
9406
9513
  }
9407
9514
  }
9515
+ const userTurns = [];
9516
+ for (const message of messages) {
9517
+ const tsUnix = unixFrom(message.timestamp);
9518
+ if (!tsUnix) {
9519
+ continue;
9520
+ }
9521
+ const role = stringField(message, "role");
9522
+ if (role === "user") {
9523
+ if (isSyntheticUserContent(stringField(message, "content"))) {
9524
+ continue;
9525
+ }
9526
+ const index = userTurns.length + 1;
9527
+ userTurns.push({
9528
+ index,
9529
+ turnId: `${ctx.sessionId}:turn:${index}`,
9530
+ startedUnix: tsUnix,
9531
+ endUnix: tsUnix
9532
+ });
9533
+ continue;
9534
+ }
9535
+ const current = userTurns[userTurns.length - 1];
9536
+ if (current && (role === "assistant" || role === "tool")) {
9537
+ current.endUnix = Math.max(current.endUnix, tsUnix);
9538
+ }
9539
+ }
9408
9540
  if (usageRows.length > 0) {
9409
9541
  for (const usage of usageRows) {
9410
- const ts = isoFromUnix(usage.last_seen) || isoFromUnix(usage.first_seen) || startedTs;
9542
+ const allocations = allocateUsageAcrossTurns(usage, userTurns);
9543
+ const metricsParts = splitMetricBag(usageMetrics2(usage), allocations.map((item) => item.fraction));
9544
+ const model = stringField(usage, "model") || ctx.model;
9545
+ const provider = stringField(usage, "billing_provider") || ctx.provider;
9546
+ const task = stringField(usage, "task") || "main";
9547
+ const billing = stringField(usage, "billing_provider") || "";
9548
+ for (let i = 0; i < allocations.length; i += 1) {
9549
+ const allocation = allocations[i];
9550
+ const metrics = metricsParts[i];
9551
+ if (!metrics || !metricBagHasWork(metrics)) {
9552
+ continue;
9553
+ }
9554
+ const ts = isoFromUnix(allocation.tsUnix) || startedTs;
9555
+ const turnId = allocation.turn?.turnId;
9556
+ const slice = allocation.turn ? `:turn:${allocation.turn.index}` : "";
9557
+ lineNumber += 1;
9558
+ const event = makeEvent({
9559
+ schemaVersion: AGENT_TIME_SCHEMA_VERSION,
9560
+ ts,
9561
+ type: "model.usage",
9562
+ source: SOURCE,
9563
+ workspaceId: ctx.workspaceId,
9564
+ project: ctx.project,
9565
+ cwd: ctx.cwd,
9566
+ sessionId: ctx.sessionId,
9567
+ turnId,
9568
+ agent: SOURCE,
9569
+ model,
9570
+ provider,
9571
+ success: true,
9572
+ metrics,
9573
+ confidence: allocations.length > 1 ? "derived" : "exact",
9574
+ refs: refs({
9575
+ sourceId: `${ctx.rawId}:model:${model || "unknown"}:${task}:${billing}${slice}`,
9576
+ hermesPlatform: ctx.platform,
9577
+ hermesProfile: ctx.profileName,
9578
+ hermesTask: stringField(usage, "task"),
9579
+ hermesBillingMode: stringField(usage, "billing_mode")
9580
+ })
9581
+ }, lineNumber, filePath, options);
9582
+ if (event) {
9583
+ events.push(event);
9584
+ }
9585
+ }
9586
+ }
9587
+ } else if ((numberField(session, "input_tokens") || 0) + (numberField(session, "output_tokens") || 0) > 0) {
9588
+ const fallbackUsage = {
9589
+ first_seen: startedUnix,
9590
+ last_seen: endedUnix || startedUnix
9591
+ };
9592
+ const allocations = allocateUsageAcrossTurns(fallbackUsage, userTurns);
9593
+ const metricsParts = splitMetricBag(usageMetrics2(session), allocations.map((item) => item.fraction));
9594
+ for (let i = 0; i < allocations.length; i += 1) {
9595
+ const allocation = allocations[i];
9596
+ const metrics = metricsParts[i];
9597
+ if (!metrics || !metricBagHasWork(metrics)) {
9598
+ continue;
9599
+ }
9600
+ const ts = isoFromUnix(allocation.tsUnix) || startedTs;
9601
+ const turnId = allocation.turn?.turnId;
9602
+ const slice = allocation.turn ? `:turn:${allocation.turn.index}` : "";
9411
9603
  lineNumber += 1;
9412
9604
  const event = makeEvent({
9413
9605
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
@@ -9418,62 +9610,72 @@ async function parseHermesDb(filePath, options) {
9418
9610
  project: ctx.project,
9419
9611
  cwd: ctx.cwd,
9420
9612
  sessionId: ctx.sessionId,
9613
+ turnId,
9421
9614
  agent: SOURCE,
9422
- model: stringField(usage, "model") || ctx.model,
9423
- provider: stringField(usage, "billing_provider") || ctx.provider,
9615
+ model: ctx.model,
9616
+ provider: ctx.provider,
9424
9617
  success: true,
9425
- metrics: usageMetrics2(usage),
9426
- confidence: "exact",
9618
+ metrics,
9619
+ confidence: "partial",
9427
9620
  refs: refs({
9428
- sourceId: `${ctx.rawId}:model:${stringField(usage, "model") || "unknown"}:${stringField(usage, "task") || "main"}:${stringField(usage, "billing_provider") || ""}`,
9621
+ sourceId: `${ctx.rawId}:model:session${slice}`,
9429
9622
  hermesPlatform: ctx.platform,
9430
- hermesProfile: ctx.profileName,
9431
- hermesTask: stringField(usage, "task"),
9432
- hermesBillingMode: stringField(usage, "billing_mode")
9623
+ hermesProfile: ctx.profileName
9433
9624
  })
9434
9625
  }, lineNumber, filePath, options);
9435
9626
  if (event) {
9436
9627
  events.push(event);
9437
9628
  }
9438
9629
  }
9439
- } else if ((numberField(session, "input_tokens") || 0) + (numberField(session, "output_tokens") || 0) > 0) {
9630
+ }
9631
+ let turnIndex = 0;
9632
+ let activeTurnId;
9633
+ let activeTurnStartedTs;
9634
+ let lastWorkTs;
9635
+ const closeActiveTurn = (ts) => {
9636
+ if (!activeTurnId) {
9637
+ return;
9638
+ }
9440
9639
  lineNumber += 1;
9441
- const event = makeEvent({
9640
+ const completed = makeEvent({
9442
9641
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
9443
- ts: endedTs || startedTs,
9444
- type: "model.usage",
9642
+ ts,
9643
+ type: "turn.completed",
9445
9644
  source: SOURCE,
9446
9645
  workspaceId: ctx.workspaceId,
9447
9646
  project: ctx.project,
9448
9647
  cwd: ctx.cwd,
9449
9648
  sessionId: ctx.sessionId,
9649
+ turnId: activeTurnId,
9450
9650
  agent: SOURCE,
9451
- model: ctx.model,
9452
- provider: ctx.provider,
9453
- success: true,
9454
- metrics: usageMetrics2(session),
9455
- confidence: "partial",
9456
- refs: refs({
9457
- sourceId: `${ctx.rawId}:model:session`,
9458
- hermesPlatform: ctx.platform,
9459
- hermesProfile: ctx.profileName
9460
- })
9651
+ confidence: "derived",
9652
+ refs: refs({ sourceId: `${ctx.rawId}:turn:${turnIndex}:end` })
9461
9653
  }, lineNumber, filePath, options);
9462
- if (event) {
9463
- events.push(event);
9654
+ if (completed) {
9655
+ events.push(completed);
9464
9656
  }
9465
- }
9466
- let turnIndex = 0;
9467
- let activeTurnId;
9657
+ activeTurnId = void 0;
9658
+ activeTurnStartedTs = void 0;
9659
+ lastWorkTs = void 0;
9660
+ };
9468
9661
  for (const message of messages) {
9469
- const ts = isoFromUnix(message.timestamp);
9470
- if (!ts) {
9662
+ const tsUnix = unixFrom(message.timestamp);
9663
+ const ts = isoFromUnix(tsUnix);
9664
+ if (!tsUnix || !ts) {
9471
9665
  continue;
9472
9666
  }
9473
9667
  const role = stringField(message, "role");
9474
9668
  if (role === "user") {
9669
+ if (isSyntheticUserContent(stringField(message, "content"))) {
9670
+ continue;
9671
+ }
9672
+ if (activeTurnId && activeTurnStartedTs) {
9673
+ closeActiveTurn(lastWorkTs || activeTurnStartedTs);
9674
+ }
9475
9675
  turnIndex += 1;
9476
9676
  activeTurnId = `${ctx.sessionId}:turn:${turnIndex}`;
9677
+ activeTurnStartedTs = ts;
9678
+ lastWorkTs = void 0;
9477
9679
  lineNumber += 1;
9478
9680
  const turnStarted = makeEvent({
9479
9681
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
@@ -9514,6 +9716,10 @@ async function parseHermesDb(filePath, options) {
9514
9716
  }
9515
9717
  continue;
9516
9718
  }
9719
+ if (role !== "assistant" && role !== "tool") {
9720
+ continue;
9721
+ }
9722
+ lastWorkTs = ts;
9517
9723
  if (role !== "assistant") {
9518
9724
  continue;
9519
9725
  }
@@ -9568,6 +9774,9 @@ async function parseHermesDb(filePath, options) {
9568
9774
  }
9569
9775
  }
9570
9776
  }
9777
+ if (activeTurnId) {
9778
+ closeActiveTurn(lastWorkTs || endedTs || startedTs);
9779
+ }
9571
9780
  }
9572
9781
  return events.sort((a, b) => a.ts.localeCompare(b.ts) || (a.id || "").localeCompare(b.id || ""));
9573
9782
  } finally {
@@ -9628,7 +9837,6 @@ description: "VibeTime session sync \u2014 thin trigger that runs local backfill
9628
9837
  author: VibeTime
9629
9838
  hooks:
9630
9839
  - on_session_start
9631
- - on_session_end
9632
9840
  - on_session_finalize
9633
9841
  - post_tool_call
9634
9842
  `;
@@ -9639,6 +9847,10 @@ function pluginInit() {
9639
9847
 
9640
9848
  Thin trigger only: spawn \`vibetime hook --agent hermes\` on session/tool
9641
9849
  lifecycle events. Token usage is read from state.db by vibetime backfill.
9850
+
9851
+ \`on_session_end\` is a per-turn observer in Hermes (end of every
9852
+ run_conversation). Do not map it to SessionEnd \u2014 that would fire a
9853
+ backfill after every prompt. Real session teardown is on_session_finalize.
9642
9854
  """
9643
9855
  from __future__ import annotations
9644
9856
 
@@ -9654,15 +9866,20 @@ def _report(payload: dict[str, Any]) -> None:
9654
9866
  if not command:
9655
9867
  return
9656
9868
  try:
9657
- subprocess.run(
9869
+ # Popen + close stdin: do not block the Hermes tool loop waiting for
9870
+ # vibetime hook / backfill. stdout/stderr inherited would spam the
9871
+ # agent UI, so discard them.
9872
+ proc = subprocess.Popen(
9658
9873
  [command, "hook", "--agent", "hermes"],
9659
- input=json.dumps(payload),
9660
- text=True,
9661
- capture_output=True,
9662
- timeout=10,
9663
- check=False,
9874
+ stdin=subprocess.PIPE,
9875
+ stdout=subprocess.DEVNULL,
9876
+ stderr=subprocess.DEVNULL,
9664
9877
  env=os.environ.copy(),
9878
+ start_new_session=True,
9665
9879
  )
9880
+ if proc.stdin is not None:
9881
+ proc.stdin.write(json.dumps(payload).encode())
9882
+ proc.stdin.close()
9666
9883
  except Exception:
9667
9884
  return
9668
9885
 
@@ -9671,8 +9888,8 @@ def on_session_start(*, session_id: str = "", **_: Any) -> None:
9671
9888
  _report({"hook_event_name": "SessionStart", "session_id": session_id})
9672
9889
 
9673
9890
 
9674
- def on_session_end(*, session_id: str = "", **_: Any) -> None:
9675
- _report({"hook_event_name": "SessionEnd", "session_id": session_id})
9891
+ def on_session_finalize(*, session_id: str = "", **_: Any) -> None:
9892
+ _report({"hook_event_name": "SessionEnd", "session_id": session_id or ""})
9676
9893
 
9677
9894
 
9678
9895
  def on_post_tool_call(*, tool_name: str = "", session_id: str = "", **_: Any) -> None:
@@ -9685,8 +9902,7 @@ def on_post_tool_call(*, tool_name: str = "", session_id: str = "", **_: Any) ->
9685
9902
 
9686
9903
  def register(ctx) -> None:
9687
9904
  ctx.register_hook("on_session_start", on_session_start)
9688
- ctx.register_hook("on_session_end", on_session_end)
9689
- ctx.register_hook("on_session_finalize", on_session_end)
9905
+ ctx.register_hook("on_session_finalize", on_session_finalize)
9690
9906
  ctx.register_hook("post_tool_call", on_post_tool_call)
9691
9907
  `;
9692
9908
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yhong91/vibetime",
3
3
  "type": "module",
4
- "version": "0.1.72",
4
+ "version": "0.1.74",
5
5
  "description": "vibetime CLI — install AI-agent hooks (Claude Code, Codex, OpenCode, Pi, Cursor) and report activity to vibetime.",
6
6
  "license": "MIT",
7
7
  "publishConfig": {