@mduan/paseo-cli 0.11.0-fork.1 → 0.11.0-fork.3

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 (68) hide show
  1. package/node_modules/@getpaseo/client/package.json +3 -3
  2. package/node_modules/@getpaseo/highlight/package.json +1 -1
  3. package/node_modules/@getpaseo/plugin/package.json +3 -3
  4. package/node_modules/@getpaseo/protocol/dist/agent-types.d.ts +3 -0
  5. package/node_modules/@getpaseo/protocol/dist/generated/validation/ws-outbound.aot.js +337 -1
  6. package/node_modules/@getpaseo/protocol/dist/messages.js +4 -0
  7. package/node_modules/@getpaseo/protocol/package.json +1 -1
  8. package/node_modules/@getpaseo/relay/package.json +1 -1
  9. package/node_modules/@getpaseo/server/dist/server/builtin-plugins/package.json +1 -1
  10. package/node_modules/@getpaseo/server/dist/server/server/agent/agent-manager.d.ts +2 -2
  11. package/node_modules/@getpaseo/server/dist/server/server/agent/agent-manager.js +5 -0
  12. package/node_modules/@getpaseo/server/dist/server/server/agent/agent-projections.js +4 -0
  13. package/node_modules/@getpaseo/server/dist/server/server/agent/agent-sdk-types.d.ts +8 -1
  14. package/node_modules/@getpaseo/server/dist/server/server/agent/agent-storage.d.ts +2 -0
  15. package/node_modules/@getpaseo/server/dist/server/server/agent/agent-storage.js +2 -0
  16. package/node_modules/@getpaseo/server/dist/server/server/agent/provider-subagents/cost.d.ts +17 -0
  17. package/node_modules/@getpaseo/server/dist/server/server/agent/provider-subagents/cost.js +25 -0
  18. package/node_modules/@getpaseo/server/dist/server/server/agent/provider-subagents/subtitle-format.d.ts +8 -0
  19. package/node_modules/@getpaseo/server/dist/server/server/agent/provider-subagents/subtitle-format.js +22 -0
  20. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/appended-lines-reader.d.ts +20 -0
  21. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/appended-lines-reader.js +59 -0
  22. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/agent.js +147 -4
  23. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/live-source.d.ts +6 -0
  24. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/live-source.js +33 -0
  25. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/presentation.d.ts +4 -0
  26. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/presentation.js +6 -8
  27. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/replay-source.d.ts +13 -0
  28. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/replay-source.js +44 -22
  29. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/transcript-cost.d.ts +18 -0
  30. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/claude/subagents/transcript-cost.js +50 -0
  31. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex/pricing.d.ts +15 -1
  32. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex/pricing.js +32 -1
  33. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex/rollout-cost.d.ts +4 -8
  34. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex/rollout-cost.js +11 -51
  35. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex-app-server-agent.d.ts +7 -1
  36. package/node_modules/@getpaseo/server/dist/server/server/agent/providers/codex-app-server-agent.js +119 -17
  37. package/node_modules/@getpaseo/server/dist/server/server/agent/turn-diffs/snapshot.d.ts +14 -0
  38. package/node_modules/@getpaseo/server/dist/server/server/agent/turn-diffs/snapshot.js +91 -4
  39. package/node_modules/@getpaseo/server/dist/server/server/agent/turn-diffs/store.d.ts +3 -3
  40. package/node_modules/@getpaseo/server/dist/server/server/agent/turn-diffs/store.js +24 -18
  41. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-930503b4f858c84de293b25a88bddcd5.js → desktop-attachment-bridge-6eaa3854bb0e0bf2260c50ed5acac196.js} +1 -1
  42. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-6eaa3854bb0e0bf2260c50ed5acac196.js.br +4 -0
  43. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-6eaa3854bb0e0bf2260c50ed5acac196.js.gz +0 -0
  44. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-46b63a296048f752e9cace2869e65020.js → desktop-attachment-store-f093e6af17b9293a6d18bd73214b3172.js} +1 -1
  45. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-46b63a296048f752e9cace2869e65020.js.br → desktop-attachment-store-f093e6af17b9293a6d18bd73214b3172.js.br} +0 -0
  46. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-f093e6af17b9293a6d18bd73214b3172.js.gz +0 -0
  47. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{index-9489c4e1587c4252f0bd3b6e4b1abab3.js → index-ac557a7bf2f9db15fc71b6d2cbb3fcf7.js} +748 -747
  48. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/index-ac557a7bf2f9db15fc71b6d2cbb3fcf7.js.br +0 -0
  49. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/index-ac557a7bf2f9db15fc71b6d2cbb3fcf7.js.gz +0 -0
  50. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-4443393854050b1919467b89f1cbafc4.js → indexeddb-attachment-store-c1b8c035b248805cc62bb0f7b8e50acc.js} +1 -1
  51. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-4443393854050b1919467b89f1cbafc4.js.br → indexeddb-attachment-store-c1b8c035b248805cc62bb0f7b8e50acc.js.br} +0 -0
  52. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-c1b8c035b248805cc62bb0f7b8e50acc.js.gz +0 -0
  53. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-d5f9e56b2ce163fcb45fa34b52e2c3d1.js → native-file-attachment-store-c3b75054cb85e7c8b21aedadaf7d54f3.js} +3 -3
  54. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c3b75054cb85e7c8b21aedadaf7d54f3.js.br +0 -0
  55. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c3b75054cb85e7c8b21aedadaf7d54f3.js.gz +0 -0
  56. package/node_modules/@getpaseo/server/dist/server/web-ui/index.html +1 -1
  57. package/node_modules/@getpaseo/server/dist/server/web-ui/index.html.br +0 -0
  58. package/node_modules/@getpaseo/server/dist/server/web-ui/index.html.gz +0 -0
  59. package/node_modules/@getpaseo/server/package.json +6 -6
  60. package/package.json +7 -7
  61. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-930503b4f858c84de293b25a88bddcd5.js.br +0 -0
  62. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-930503b4f858c84de293b25a88bddcd5.js.gz +0 -0
  63. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-46b63a296048f752e9cace2869e65020.js.gz +0 -0
  64. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/index-9489c4e1587c4252f0bd3b6e4b1abab3.js.br +0 -0
  65. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/index-9489c4e1587c4252f0bd3b6e4b1abab3.js.gz +0 -0
  66. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-4443393854050b1919467b89f1cbafc4.js.gz +0 -0
  67. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-d5f9e56b2ce163fcb45fa34b52e2c3d1.js.br +0 -0
  68. package/node_modules/@getpaseo/server/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-d5f9e56b2ce163fcb45fa34b52e2c3d1.js.gz +0 -0
@@ -11,8 +11,10 @@ import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
11
11
  import { composeSystemPromptParts } from "../system-prompt.js";
12
12
  import { curateAgentActivity } from "../activity-curator.js";
13
13
  import { CodexAsyncQuestions, codexAsyncQuestionToTimeline } from "./codex/async-questions.js";
14
- import { CodexPriceList } from "./codex/pricing.js";
14
+ import { getSharedCodexPriceList } from "./codex/pricing.js";
15
15
  import { CodexRolloutCost } from "./codex/rollout-cost.js";
16
+ import { rollupProviderSubagentCosts } from "../provider-subagents/cost.js";
17
+ import { formatSubagentCosts, formatSubagentTokens, } from "../provider-subagents/subtitle-format.js";
16
18
  import { mapCodexToolCallEnvelope, mapCodexToolCallFromThreadItem, normalizeCommandExecutionCommand, splitCodexMcpToolResultImages, } from "./codex/tool-call-mapper.js";
17
19
  import { checkProviderLaunchAvailable, createProviderEnv, createProviderEnvSpec, resolveProviderLaunch, } from "../provider-launch-config.js";
18
20
  import { findExecutable, probeExecutable, } from "../../../executable-resolution/executable-resolution.js";
@@ -723,12 +725,6 @@ function filterCodexThreadsByCwd(threads, cwd) {
723
725
  const belongsToWorkspace = createPathEquivalenceMatcher(cwd);
724
726
  return threads.filter((thread) => typeof thread.cwd === "string" && belongsToWorkspace(thread.cwd));
725
727
  }
726
- let sharedCodexPriceList;
727
- // One price list per daemon, so every Codex session and custom Codex provider shares one fetch.
728
- function getSharedCodexPriceList(logger) {
729
- sharedCodexPriceList ?? (sharedCodexPriceList = new CodexPriceList({ logger }));
730
- return sharedCodexPriceList;
731
- }
732
728
  var CodexCostStatus;
733
729
  (function (CodexCostStatus) {
734
730
  /** No priced request yet, an unpriced model, or no rollout file. */
@@ -750,16 +746,34 @@ function isSameCostEstimate(left, right) {
750
746
  }
751
747
  return left.status === right.status;
752
748
  }
753
- // Both cost fields are always written, undefined included, so the agent manager's usage merge on
749
+ // All cost fields are always written, undefined included, so the agent manager's usage merge on
754
750
  // turn completion cannot keep a stale value from the previous state.
755
- function withCostEstimate(usage, estimate) {
751
+ function withCostEstimate(usage, estimate, subagentCostUsd) {
756
752
  switch (estimate.status) {
757
753
  case CodexCostStatus.Estimated:
758
- return { ...usage, totalCostUsd: estimate.costUsd, totalCostUnavailable: false };
754
+ return {
755
+ ...usage,
756
+ ...(subagentCostUsd > 0 ? { subagentCostUsd } : { subagentCostUsd: undefined }),
757
+ totalCostUsd: estimate.costUsd + subagentCostUsd,
758
+ totalCostUnavailable: false,
759
+ totalCostEstimated: true,
760
+ };
759
761
  case CodexCostStatus.Unavailable:
760
- return { ...usage, totalCostUsd: undefined, totalCostUnavailable: true };
762
+ return {
763
+ ...usage,
764
+ subagentCostUsd: undefined,
765
+ totalCostUsd: undefined,
766
+ totalCostUnavailable: true,
767
+ totalCostEstimated: undefined,
768
+ };
761
769
  case CodexCostStatus.Pending:
762
- return { ...usage, totalCostUsd: undefined, totalCostUnavailable: undefined };
770
+ return {
771
+ ...usage,
772
+ subagentCostUsd: undefined,
773
+ totalCostUsd: undefined,
774
+ totalCostUnavailable: undefined,
775
+ totalCostEstimated: undefined,
776
+ };
763
777
  }
764
778
  }
765
779
  export function toAgentUsage(tokenUsage) {
@@ -2598,6 +2612,7 @@ export class CodexAppServerAgentSession {
2598
2612
  this.emittedProviderSubagentUserMessageKeys = new Set();
2599
2613
  this.subAgentCallsByCallId = new Map();
2600
2614
  this.subAgentCallIdByChildThreadId = new Map();
2615
+ this.subAgentUsageByThreadId = new Map();
2601
2616
  this.pendingSubAgentNotificationsByThreadId = new Map();
2602
2617
  this.warnedUnknownNotificationMethods = new Set();
2603
2618
  this.warnedInvalidNotificationPayloads = new Set();
@@ -3000,6 +3015,7 @@ export class CodexAppServerAgentSession {
3000
3015
  const { timeline, subAgentRoutes } = history;
3001
3016
  this.subAgentCallsByCallId.clear();
3002
3017
  this.subAgentCallIdByChildThreadId.clear();
3018
+ this.subAgentUsageByThreadId.clear();
3003
3019
  this.pendingSubAgentNotificationsByThreadId.clear();
3004
3020
  this.persistedProviderSubagentEvents = [];
3005
3021
  this.loadingPersistedHistory = true;
@@ -3041,6 +3057,9 @@ export class CodexAppServerAgentSession {
3041
3057
  parentSubagentId: next.parentSubagentId,
3042
3058
  });
3043
3059
  try {
3060
+ const usage = {};
3061
+ this.subAgentUsageByThreadId.set(next.route.childThreadId, usage);
3062
+ await this.refreshSubAgentCost(next.route.childThreadId, usage, client);
3044
3063
  const childHistory = await loadCodexThreadHistoryTimeline({
3045
3064
  threadId: next.route.childThreadId,
3046
3065
  cwd: this.config.cwd ?? null,
@@ -4271,6 +4290,9 @@ export class CodexAppServerAgentSession {
4271
4290
  }
4272
4291
  dispatchSubAgentNotification(parsed, callId) {
4273
4292
  switch (parsed.kind) {
4293
+ case "token_usage_updated":
4294
+ this.handleSubAgentTokenUsage(parsed);
4295
+ return;
4274
4296
  case "thread_started":
4275
4297
  this.emitSubAgentActivityUpdate(callId, "running", { reopen: true });
4276
4298
  return;
@@ -4937,7 +4959,10 @@ export class CodexAppServerAgentSession {
4937
4959
  }
4938
4960
  handleTokenUsageUpdatedNotification(parsed) {
4939
4961
  const usage = toAgentUsage(parsed.tokenUsage);
4940
- this.latestUsage = usage ? withCostEstimate(usage, this.costEstimate) : undefined;
4962
+ const subagentCostUsd = this.subagentCostRollup().totalCostUsd;
4963
+ this.latestUsage = usage
4964
+ ? withCostEstimate(usage, this.costEstimate, subagentCostUsd)
4965
+ : undefined;
4941
4966
  if (this.latestUsage) {
4942
4967
  this.notifySubscribers({
4943
4968
  type: "usage_updated",
@@ -4947,6 +4972,83 @@ export class CodexAppServerAgentSession {
4947
4972
  }
4948
4973
  void this.refreshRolloutCost();
4949
4974
  }
4975
+ handleSubAgentTokenUsage(parsed) {
4976
+ const threadId = parsed.threadId;
4977
+ if (!threadId)
4978
+ return;
4979
+ let usage = this.subAgentUsageByThreadId.get(threadId);
4980
+ if (!usage) {
4981
+ usage = {};
4982
+ this.subAgentUsageByThreadId.set(threadId, usage);
4983
+ }
4984
+ // The last request's context, which is what the Claude subagent rows report as tokens too.
4985
+ usage.tokens = toAgentUsage(parsed.tokenUsage)?.contextWindowUsedTokens;
4986
+ this.emitSubAgentSubtitle(threadId, usage);
4987
+ void this.refreshSubAgentCost(threadId, usage);
4988
+ }
4989
+ async refreshSubAgentCost(threadId, usage, client = this.client) {
4990
+ const priceList = this.deps.codexPriceList;
4991
+ if (!priceList)
4992
+ return;
4993
+ try {
4994
+ usage.rolloutCost ?? (usage.rolloutCost = this.openRolloutCost(threadId, client));
4995
+ const rolloutCost = await usage.rolloutCost;
4996
+ const prices = await priceList.get();
4997
+ if (!rolloutCost || !prices)
4998
+ return;
4999
+ usage.costUsd = await rolloutCost.read({ prices, serviceTier: this.serviceTier });
5000
+ usage.model = rolloutCost.model;
5001
+ if (this.subAgentUsageByThreadId.get(threadId) !== usage)
5002
+ return;
5003
+ this.refreshCostPresentation();
5004
+ }
5005
+ catch (error) {
5006
+ usage.rolloutCost = undefined;
5007
+ this.logger.warn({ error, threadId }, "Failed to read a Codex subagent rollout for its cost");
5008
+ }
5009
+ }
5010
+ emitSubAgentSubtitle(threadId, usage, subagentCostUsd = 0) {
5011
+ // A provider subtitle replaces the row's agent type, so it leads with it.
5012
+ const callId = this.subAgentCallIdByChildThreadId.get(threadId);
5013
+ const detail = callId ? this.subAgentCallsByCallId.get(callId)?.toolCall.detail : undefined;
5014
+ const subtitle = [
5015
+ detail?.type === "sub_agent" ? (detail.subAgentType ?? "Codex subagent") : undefined,
5016
+ usage.model,
5017
+ formatSubagentTokens(usage.tokens),
5018
+ ...formatSubagentCosts({ ownCostUsd: usage.costUsd, subagentCostUsd }),
5019
+ ]
5020
+ .filter((part) => part !== undefined)
5021
+ .join(" · ");
5022
+ if (!subtitle || subtitle === usage.subtitle)
5023
+ return;
5024
+ usage.subtitle = subtitle;
5025
+ this.emitEvent({
5026
+ type: "provider_subagent",
5027
+ provider: CODEX_PROVIDER,
5028
+ event: { type: "upsert", id: threadId, subtitle },
5029
+ });
5030
+ }
5031
+ subagentCostRollup() {
5032
+ return rollupProviderSubagentCosts([...this.subAgentCallIdByChildThreadId].map(([threadId, callId]) => ({
5033
+ id: threadId,
5034
+ parentSubagentId: this.subAgentCallsByCallId.get(callId)?.parentSubagentId ?? null,
5035
+ ownCostUsd: this.subAgentUsageByThreadId.get(threadId)?.costUsd ?? 0,
5036
+ })));
5037
+ }
5038
+ refreshCostPresentation() {
5039
+ const costs = this.subagentCostRollup();
5040
+ for (const [threadId, usage] of this.subAgentUsageByThreadId) {
5041
+ this.emitSubAgentSubtitle(threadId, usage, costs.byId.get(threadId)?.subagentCostUsd);
5042
+ }
5043
+ if (!this.latestUsage)
5044
+ return;
5045
+ this.latestUsage = withCostEstimate(this.latestUsage, this.costEstimate, costs.totalCostUsd);
5046
+ this.notifySubscribers({
5047
+ type: "usage_updated",
5048
+ provider: CODEX_PROVIDER,
5049
+ usage: this.latestUsage,
5050
+ });
5051
+ }
4950
5052
  async refreshRolloutCost() {
4951
5053
  const threadId = this.currentThreadId;
4952
5054
  const priceList = this.deps.codexPriceList;
@@ -4979,17 +5081,17 @@ export class CodexAppServerAgentSession {
4979
5081
  this.costEstimate = estimate;
4980
5082
  if (!this.latestUsage)
4981
5083
  return;
4982
- this.latestUsage = withCostEstimate(this.latestUsage, estimate);
5084
+ this.latestUsage = withCostEstimate(this.latestUsage, estimate, this.subagentCostRollup().totalCostUsd);
4983
5085
  this.notifySubscribers({
4984
5086
  type: "usage_updated",
4985
5087
  provider: CODEX_PROVIDER,
4986
5088
  usage: this.latestUsage,
4987
5089
  });
4988
5090
  }
4989
- async openRolloutCost(threadId) {
4990
- if (!this.client)
5091
+ async openRolloutCost(threadId, client = this.client) {
5092
+ if (!client)
4991
5093
  return undefined;
4992
- const response = toObjectRecord(await this.client.request("thread/read", { threadId, includeTurns: false }));
5094
+ const response = toObjectRecord(await client.request("thread/read", { threadId, includeTurns: false }));
4993
5095
  // Thread.path is marked unstable in the app-server schema; without it there is no estimate.
4994
5096
  const rolloutPath = toObjectRecord(response?.thread)?.path;
4995
5097
  return typeof rolloutPath === "string" ? new CodexRolloutCost(rolloutPath) : undefined;
@@ -3,12 +3,26 @@ export interface WorkingTreeSnapshot {
3
3
  repoRoot: string;
4
4
  tree: string;
5
5
  }
6
+ export type TurnStartSnapshot = WorkingTreeSnapshot & {
7
+ head?: string;
8
+ headLogPath: string;
9
+ headLogSize: number;
10
+ };
6
11
  /**
7
12
  * Writes the whole working tree (tracked changes plus untracked, non-ignored files up to 1 MiB)
8
13
  * as a git tree object. Staging goes through a copy of the index, so the user's index, HEAD and
9
14
  * branches are untouched; only git objects are written.
10
15
  */
11
16
  export declare function snapshotWorkingTree(cwd: string): Promise<WorkingTreeSnapshot | null>;
17
+ /** A working tree snapshot plus where HEAD and its reflog stood, to separate out branch moves. */
18
+ export declare function snapshotTurnStart(cwd: string): Promise<TurnStartSnapshot | null>;
19
+ /**
20
+ * The tree to diff a turn from. When the turn moved HEAD to another commit, the start snapshot's
21
+ * uncommitted changes are replayed onto where HEAD landed, so the diff holds the agent's edits and
22
+ * commits and none of the content the move brought in. Undefined when that can't be worked out
23
+ * (no reflog, unborn branch, replay conflict); the turn then gets no diff.
24
+ */
25
+ export declare function resolveTurnFromTree(start: TurnStartSnapshot): Promise<string | undefined>;
12
26
  export declare function diffTreeStats(params: {
13
27
  repoRoot: string;
14
28
  fromTree: string;
@@ -15,10 +15,14 @@ async function git(args, options) {
15
15
  }
16
16
  async function resolveRepo(cwd) {
17
17
  try {
18
- const [repoRoot, indexPath] = (await git(["rev-parse", "--show-toplevel", "--git-path", "index"], { cwd }))
18
+ const [repoRoot, indexPath, headLogPath] = (await git(["rev-parse", "--show-toplevel", "--git-path", "index", "--git-path", "logs/HEAD"], { cwd }))
19
19
  .trim()
20
20
  .split("\n");
21
- return { repoRoot, indexPath: path.resolve(cwd, indexPath) };
21
+ return {
22
+ repoRoot,
23
+ indexPath: path.resolve(cwd, indexPath),
24
+ headLogPath: path.resolve(cwd, headLogPath),
25
+ };
22
26
  }
23
27
  catch {
24
28
  // Not a git repository (or git is missing): turn diffs are git-only.
@@ -43,6 +47,19 @@ export async function snapshotWorkingTree(cwd) {
43
47
  const repo = await resolveRepo(cwd);
44
48
  if (!repo)
45
49
  return null;
50
+ return { repoRoot: repo.repoRoot, tree: await writeWorkingTree(repo) };
51
+ }
52
+ /** A working tree snapshot plus where HEAD and its reflog stood, to separate out branch moves. */
53
+ export async function snapshotTurnStart(cwd) {
54
+ const repo = await resolveRepo(cwd);
55
+ if (!repo)
56
+ return null;
57
+ const headLogSize = await fs.stat(repo.headLogPath).then((stat) => stat.size, () => 0);
58
+ const head = await readHead(repo.repoRoot);
59
+ const tree = await writeWorkingTree(repo);
60
+ return { repoRoot: repo.repoRoot, tree, head, headLogPath: repo.headLogPath, headLogSize };
61
+ }
62
+ async function writeWorkingTree(repo) {
46
63
  const tempIndex = path.join(os.tmpdir(), `paseo-turn-index-${randomUUID()}`);
47
64
  try {
48
65
  const stat = await fs.stat(repo.indexPath).catch(() => null);
@@ -61,13 +78,83 @@ export async function snapshotWorkingTree(cwd) {
61
78
  input: untracked.map((file) => `${file}\0`).join(""),
62
79
  });
63
80
  }
64
- const tree = (await git(["write-tree"], { cwd: repo.repoRoot, env })).trim();
65
- return { repoRoot: repo.repoRoot, tree };
81
+ return (await git(["write-tree"], { cwd: repo.repoRoot, env })).trim();
66
82
  }
67
83
  finally {
68
84
  await fs.rm(tempIndex, { force: true });
69
85
  }
70
86
  }
87
+ async function readHead(repoRoot) {
88
+ try {
89
+ return (await git(["rev-parse", "--verify", "-q", "HEAD"], { cwd: repoRoot })).trim();
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
95
+ async function readFileFrom(filePath, offset) {
96
+ const handle = await fs.open(filePath, "r").catch(() => null);
97
+ if (!handle)
98
+ return undefined;
99
+ try {
100
+ const { size } = await handle.stat();
101
+ if (size < offset)
102
+ return undefined;
103
+ const buffer = Buffer.alloc(size - offset);
104
+ await handle.read(buffer, 0, buffer.length, offset);
105
+ return buffer.toString("utf8");
106
+ }
107
+ finally {
108
+ await handle.close();
109
+ }
110
+ }
111
+ const OWN_COMMIT_MESSAGE = /^commit( \((initial|amend)\))?: /;
112
+ /**
113
+ * The commit HEAD landed on after the turn's last branch move (checkout, reset, pull, merge,
114
+ * rebase, cherry-pick), read from the reflog lines the turn appended. Undefined when HEAD only
115
+ * moved by the agent's own commits; null when the reflog can't tell.
116
+ */
117
+ async function findLastBranchMove(start) {
118
+ const appended = await readFileFrom(start.headLogPath, start.headLogSize);
119
+ if (appended === undefined)
120
+ return null;
121
+ let landing;
122
+ for (const line of appended.split("\n")) {
123
+ // <old> <new> <identity> <time> <tz>\t<message>
124
+ const [header, message = ""] = line.split("\t", 2);
125
+ const [oldHead, newHead] = header.split(" ");
126
+ if (!newHead || oldHead === newHead || OWN_COMMIT_MESSAGE.test(message))
127
+ continue;
128
+ landing = newHead;
129
+ }
130
+ return landing;
131
+ }
132
+ /**
133
+ * The tree to diff a turn from. When the turn moved HEAD to another commit, the start snapshot's
134
+ * uncommitted changes are replayed onto where HEAD landed, so the diff holds the agent's edits and
135
+ * commits and none of the content the move brought in. Undefined when that can't be worked out
136
+ * (no reflog, unborn branch, replay conflict); the turn then gets no diff.
137
+ */
138
+ export async function resolveTurnFromTree(start) {
139
+ const head = await readHead(start.repoRoot);
140
+ if (head === start.head)
141
+ return start.tree;
142
+ if (!head || !start.head)
143
+ return undefined;
144
+ const landing = await findLastBranchMove(start);
145
+ if (landing === null)
146
+ return undefined;
147
+ if (landing === undefined || landing === start.head)
148
+ return start.tree;
149
+ try {
150
+ const output = await git(["merge-tree", "--write-tree", `--merge-base=${start.head}`, landing, start.tree], { cwd: start.repoRoot });
151
+ return output.split("\n")[0];
152
+ }
153
+ catch {
154
+ // Exit code 1: the start's uncommitted changes conflict with where HEAD landed.
155
+ return undefined;
156
+ }
157
+ }
71
158
  export async function diffTreeStats(params) {
72
159
  if (params.fromTree === params.toTree)
73
160
  return [];
@@ -1,6 +1,6 @@
1
1
  import type { Logger } from "pino";
2
2
  import { type AgentTurnDiffTarget, type TurnDiffSummary } from "../../messages.js";
3
- import { type WorkingTreeSnapshot } from "./snapshot.js";
3
+ import { type TurnStartSnapshot } from "./snapshot.js";
4
4
  export interface TurnDiffRange {
5
5
  repoRoot: string;
6
6
  fromTree: string;
@@ -8,7 +8,7 @@ export interface TurnDiffRange {
8
8
  }
9
9
  export interface RecordTurnInput {
10
10
  agentId: string;
11
- start: WorkingTreeSnapshot;
11
+ start: TurnStartSnapshot;
12
12
  turnId: string;
13
13
  userMessageIds: string[];
14
14
  }
@@ -21,7 +21,7 @@ export declare class TurnDiffStore {
21
21
  private readonly logger;
22
22
  private readonly queues;
23
23
  constructor(dir: string, logger: Logger);
24
- captureStart(cwd: string): Promise<WorkingTreeSnapshot | null>;
24
+ captureStart(cwd: string): Promise<TurnStartSnapshot | null>;
25
25
  recordTurn(input: RecordTurnInput): Promise<void>;
26
26
  list(agentId: string): Promise<TurnDiffSummary[]>;
27
27
  resolveRange(agentId: string, target: AgentTurnDiffTarget): Promise<TurnDiffRange | null>;
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  import { withTimeout } from "../../../utils/promise-timeout.js";
5
5
  import { writeJsonFileAtomic } from "../../atomic-file.js";
6
6
  import { TurnDiffFileStatSchema, } from "../../messages.js";
7
- import { diffTreeStats, snapshotWorkingTree, syncTreeRefs, } from "./snapshot.js";
7
+ import { diffTreeStats, resolveTurnFromTree, snapshotTurnStart, snapshotWorkingTree, syncTreeRefs, } from "./snapshot.js";
8
8
  // The prompt waits for the start snapshot; past this, the turn goes without a diff.
9
9
  const START_SNAPSHOT_TIMEOUT_MS = 2000;
10
10
  const MAX_STORED_TURNS = 200;
@@ -17,15 +17,18 @@ const StoredTurnSchema = TreeRangeSchema.extend({
17
17
  });
18
18
  const TurnDiffRecordSchema = z.object({
19
19
  repoRoot: z.string(),
20
- // First snapshot of the chat; "Chat Session" diffs from here. Never trimmed.
21
- sessionFromTree: z.string(),
22
- // Most recent finished turn, kept even when it changed nothing.
23
- lastTurn: TreeRangeSchema,
20
+ // Most recent finished turn, kept even when it changed nothing. Absent when the turn moved HEAD
21
+ // in a way its diff couldn't be separated from.
22
+ lastTurn: TreeRangeSchema.optional(),
24
23
  // Turns that changed files, oldest first.
25
24
  turns: z.array(StoredTurnSchema),
26
25
  });
27
26
  function referencedTrees(record) {
28
- const trees = new Set([record.sessionFromTree, record.lastTurn.fromTree, record.lastTurn.toTree]);
27
+ const trees = new Set();
28
+ if (record.lastTurn) {
29
+ trees.add(record.lastTurn.fromTree);
30
+ trees.add(record.lastTurn.toTree);
31
+ }
29
32
  for (const turn of record.turns) {
30
33
  trees.add(turn.fromTree);
31
34
  trees.add(turn.toTree);
@@ -53,7 +56,7 @@ export class TurnDiffStore {
53
56
  async captureStart(cwd) {
54
57
  try {
55
58
  return await withTimeout({
56
- promise: snapshotWorkingTree(cwd),
59
+ promise: snapshotTurnStart(cwd),
57
60
  timeoutMs: START_SNAPSHOT_TIMEOUT_MS,
58
61
  label: "Turn start snapshot",
59
62
  });
@@ -77,9 +80,11 @@ export class TurnDiffStore {
77
80
  const { repoRoot } = record;
78
81
  switch (target.kind) {
79
82
  case "last_turn":
80
- return { repoRoot, ...record.lastTurn };
83
+ return record.lastTurn ? { repoRoot, ...record.lastTurn } : null;
84
+ // COMPAT(turnDiffSession): added in v0.11.0-beta.3, remove after 2027-01-04. Older clients
85
+ // can still ask for the removed Chat Session diff; it is always unavailable.
81
86
  case "session":
82
- return { repoRoot, fromTree: record.sessionFromTree, toTree: record.lastTurn.toTree };
87
+ return null;
83
88
  case "turn": {
84
89
  const turn = record.turns.find((candidate) => candidate.turnId === target.turnId);
85
90
  return turn ? { repoRoot, fromTree: turn.fromTree, toTree: turn.toTree } : null;
@@ -99,26 +104,27 @@ export class TurnDiffStore {
99
104
  const end = await snapshotWorkingTree(start.repoRoot);
100
105
  if (!end)
101
106
  return;
102
- const files = await diffTreeStats({
103
- repoRoot: start.repoRoot,
104
- fromTree: start.tree,
105
- toTree: end.tree,
106
- });
107
+ const fromTree = await resolveTurnFromTree(start);
108
+ if (!fromTree) {
109
+ this.logger.info({ agentId, turnId }, "Turn moved HEAD in a way its diff can't separate");
110
+ }
111
+ const files = fromTree
112
+ ? await diffTreeStats({ repoRoot: start.repoRoot, fromTree, toTree: end.tree })
113
+ : [];
107
114
  const previous = await this.read(agentId);
108
115
  const isSameRepo = previous?.repoRoot === start.repoRoot;
109
116
  const record = {
110
117
  repoRoot: start.repoRoot,
111
- sessionFromTree: isSameRepo ? previous.sessionFromTree : start.tree,
112
- lastTurn: { fromTree: start.tree, toTree: end.tree },
118
+ lastTurn: fromTree ? { fromTree, toTree: end.tree } : undefined,
113
119
  turns: isSameRepo ? previous.turns : [],
114
120
  };
115
- if (files.length > 0) {
121
+ if (fromTree && files.length > 0) {
116
122
  const completedAt = new Date().toISOString();
117
123
  const turn = {
118
124
  turnId,
119
125
  userMessageIds,
120
126
  completedAt,
121
- fromTree: start.tree,
127
+ fromTree,
122
128
  toTree: end.tree,
123
129
  files,
124
130
  };
@@ -1 +1 @@
1
- __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentBridge=function(){const l=(0,s.createDesktopPreviewUrlResolver)({reader:(0,s.createDesktopFileReader)(),objectUrls:(0,s.createBrowserObjectUrlMinter)()});return{copyFile:t.copyDesktopAttachmentFile,writeBase64:t.writeDesktopAttachmentBase64,writeBytes:t.writeDesktopAttachmentBytes,deleteFile:t.deleteDesktopAttachmentFile,garbageCollect:t.garbageCollectDesktopAttachmentFiles,readFileBase64:s.readDesktopFileBase64,resolvePreviewUrl:t=>l.resolve(t),releasePreviewUrl:t=>l.release(t)}};var t=r(d[0]),s=r(d[1])},5137,[4428,4429]);
1
+ __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentBridge=function(){const l=(0,s.createDesktopPreviewUrlResolver)({reader:(0,s.createDesktopFileReader)(),objectUrls:(0,s.createBrowserObjectUrlMinter)()});return{copyFile:t.copyDesktopAttachmentFile,writeBase64:t.writeDesktopAttachmentBase64,writeBytes:t.writeDesktopAttachmentBytes,deleteFile:t.deleteDesktopAttachmentFile,garbageCollect:t.garbageCollectDesktopAttachmentFiles,readFileBase64:s.readDesktopFileBase64,resolvePreviewUrl:t=>l.resolve(t),releasePreviewUrl:t=>l.release(t)}};var t=r(d[0]),s=r(d[1])},5138,[4429,4430]);
@@ -0,0 +1,4 @@
1
+ � ���sZ+\n��Y��K
2
+
3
+ ,��l!�`���:۫|D�Z���
4
+ ;�qa���L�[��d��:M�8\��,� *�w�N��AQkH�ɓP������߰8��@���.e3u��U�`���;E���;Ҙ_AL�i��|�v��vgQq�os��të�E�b�>@�B��s�l�(Q²,� ��[�7(��bdt�o���\-����ں^�D��Gi�MX+�$���T��h��;Ҝ!@����mTJEVkjF����D*-�N�N���
@@ -1 +1 @@
1
- __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentStore=function(n){return{storageType:"desktop-file",async save(s){const o=s.id??(0,t.generateAttachmentId)(),l=s.fileName??null;if("file_uri"===s.source.kind)return await c(n,{id:o,uri:s.source.uri,mimeType:s.mimeType,fileName:l});if("data_url"===s.source.kind){const c=(0,t.parseDataUrl)(s.source.dataUrl),p=(0,t.normalizeMimeType)(s.mimeType??c.mimeType);return await y(n,{id:o,base64:c.base64,mimeType:p,fileName:l})}if("bytes"===s.source.kind){const c=(0,t.normalizeMimeType)(s.mimeType);return await p(n,{id:o,bytes:s.source.bytes,mimeType:c,fileName:l})}const f=(0,t.normalizeMimeType)(s.mimeType??s.source.blob.type),u=await(0,t.blobToBase64)(s.source.blob);return await y(n,{id:o,base64:u,mimeType:f,fileName:l})},encodeBase64:async({attachment:t})=>(f(t),await n.readFileBase64(t.storageKey)),resolvePreviewUrl:async({attachment:t})=>(f(t),await n.resolvePreviewUrl(t)),async releasePreviewUrl({attachment:t,url:s}){f(t),await n.releasePreviewUrl({url:s})},async delete({attachment:t}){f(t),await n.deleteFile({path:t.storageKey})},async garbageCollect({referencedIds:t}){await n.garbageCollect({referencedIds:Array.from(t)})}}};var t=r(d[0]);const n={"image/png":".png","image/jpeg":".jpg","image/jpg":".jpg","image/gif":".gif","image/webp":".webp","image/avif":".avif","image/heic":".heic","image/heif":".heif","image/tiff":".tiff","image/bmp":".bmp","image/svg+xml":".svg"};function s(t){const n=t.replace(/\\/g,"/").split("/"),s=n[n.length-1];return s&&s.length>0?s:null}function o(s){const o=(0,t.getFileExtensionFromName)(s.fileName);if(o)return o;const l=(0,t.getFileExtensionFromName)(s.sourcePath);return l||(n[s.mimeType]??".img")}function l(t){return{id:t.id,mimeType:t.mimeType,storageType:"desktop-file",storageKey:t.storagePath,fileName:t.fileName??null,byteSize:t.byteSize,createdAt:Date.now()}}async function c(n,c){const y=(0,t.fileUriToPath)(c.uri),p=c.fileName??s(y),f=(0,t.normalizeMimeType)(c.mimeType),u=o({fileName:p,sourcePath:y,mimeType:f}),T=await n.copyFile({attachmentId:c.id,sourcePath:y,extension:u});return l({id:c.id,mimeType:f,storagePath:T.path,byteSize:T.byteSize,fileName:p})}async function y(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBase64({attachmentId:n.id,base64:n.base64,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}async function p(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBytes({attachmentId:n.id,bytes:n.bytes,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}function f(t){if("desktop-file"!==t.storageType)throw new Error(`Unsupported desktop attachment storage type '${t.storageType}'.`)}},5136,[4247]);
1
+ __d(function(g,r,i,a,m,e,d){"use strict";Object.defineProperty(e,'__esModule',{value:!0}),e.createDesktopAttachmentStore=function(n){return{storageType:"desktop-file",async save(s){const o=s.id??(0,t.generateAttachmentId)(),l=s.fileName??null;if("file_uri"===s.source.kind)return await c(n,{id:o,uri:s.source.uri,mimeType:s.mimeType,fileName:l});if("data_url"===s.source.kind){const c=(0,t.parseDataUrl)(s.source.dataUrl),p=(0,t.normalizeMimeType)(s.mimeType??c.mimeType);return await y(n,{id:o,base64:c.base64,mimeType:p,fileName:l})}if("bytes"===s.source.kind){const c=(0,t.normalizeMimeType)(s.mimeType);return await p(n,{id:o,bytes:s.source.bytes,mimeType:c,fileName:l})}const f=(0,t.normalizeMimeType)(s.mimeType??s.source.blob.type),u=await(0,t.blobToBase64)(s.source.blob);return await y(n,{id:o,base64:u,mimeType:f,fileName:l})},encodeBase64:async({attachment:t})=>(f(t),await n.readFileBase64(t.storageKey)),resolvePreviewUrl:async({attachment:t})=>(f(t),await n.resolvePreviewUrl(t)),async releasePreviewUrl({attachment:t,url:s}){f(t),await n.releasePreviewUrl({url:s})},async delete({attachment:t}){f(t),await n.deleteFile({path:t.storageKey})},async garbageCollect({referencedIds:t}){await n.garbageCollect({referencedIds:Array.from(t)})}}};var t=r(d[0]);const n={"image/png":".png","image/jpeg":".jpg","image/jpg":".jpg","image/gif":".gif","image/webp":".webp","image/avif":".avif","image/heic":".heic","image/heif":".heif","image/tiff":".tiff","image/bmp":".bmp","image/svg+xml":".svg"};function s(t){const n=t.replace(/\\/g,"/").split("/"),s=n[n.length-1];return s&&s.length>0?s:null}function o(s){const o=(0,t.getFileExtensionFromName)(s.fileName);if(o)return o;const l=(0,t.getFileExtensionFromName)(s.sourcePath);return l||(n[s.mimeType]??".img")}function l(t){return{id:t.id,mimeType:t.mimeType,storageType:"desktop-file",storageKey:t.storagePath,fileName:t.fileName??null,byteSize:t.byteSize,createdAt:Date.now()}}async function c(n,c){const y=(0,t.fileUriToPath)(c.uri),p=c.fileName??s(y),f=(0,t.normalizeMimeType)(c.mimeType),u=o({fileName:p,sourcePath:y,mimeType:f}),T=await n.copyFile({attachmentId:c.id,sourcePath:y,extension:u});return l({id:c.id,mimeType:f,storagePath:T.path,byteSize:T.byteSize,fileName:p})}async function y(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBase64({attachmentId:n.id,base64:n.base64,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}async function p(t,n){const s=o({fileName:n.fileName,mimeType:n.mimeType}),c=await t.writeBytes({attachmentId:n.id,bytes:n.bytes,extension:s});return l({id:n.id,mimeType:n.mimeType,storagePath:c.path,byteSize:c.byteSize,fileName:n.fileName})}function f(t){if("desktop-file"!==t.storageType)throw new Error(`Unsupported desktop attachment storage type '${t.storageType}'.`)}},5137,[4247]);