@p4code/cli 0.1.9 → 0.1.11

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 (87) hide show
  1. package/dist/bin.mjs +1708 -1377
  2. package/dist/client/assets/{DiffPanel-Bb72saoS.js → DiffPanel-1GboPEhP.js} +2 -2
  3. package/dist/client/assets/{FilePreviewPanel-6mJ-Vsub.js → FilePreviewPanel-BZACyKlO.js} +2 -2
  4. package/dist/client/assets/{PreviewPanel-ogPpU8Ev.js → PreviewPanel-By-_LFU1.js} +2 -2
  5. package/dist/client/assets/{angular-html-BIzUKsg7.js → angular-html-DWf2nx2U.js} +2 -2
  6. package/dist/client/assets/{angular-ts-CSIA-4Zf.js → angular-ts-CIx5TGhT.js} +2 -2
  7. package/dist/client/assets/{apl-Du1uuPDy.js → apl-DafQ1-an.js} +2 -2
  8. package/dist/client/assets/arrow-right-qu8MQub2.js +2 -0
  9. package/dist/client/assets/{astro-bVpCfC8I.js → astro-Bep3tY5t.js} +2 -2
  10. package/dist/client/assets/{blade-KOd_hdA5.js → blade-DCuMesAS.js} +2 -2
  11. package/dist/client/assets/{c-BUOUjYKH.js → c-DZs6Etpe.js} +2 -2
  12. package/dist/client/assets/{cobol-DVr6VPEj.js → cobol-DFZb1h1z.js} +2 -2
  13. package/dist/client/assets/{coffee-CaWTiLWu.js → coffee-CR0gjEco.js} +2 -2
  14. package/dist/client/assets/{cpp-DOVxTtfq.js → cpp-U48aiClT.js} +2 -2
  15. package/dist/client/assets/{crystal-D_Q817Qu.js → crystal-CNaWREX9.js} +2 -2
  16. package/dist/client/assets/{css-Rez0Ry86.js → css-DQTD4ObL.js} +2 -2
  17. package/dist/client/assets/{edge-C5CKZJyo.js → edge-CGgCvH7u.js} +2 -2
  18. package/dist/client/assets/{elixir-D_eVXxgH.js → elixir-QEF-Vb-O.js} +2 -2
  19. package/dist/client/assets/{elm-BLkMkUJ0.js → elm-UIYYfgyY.js} +2 -2
  20. package/dist/client/assets/{erb-DGG0CxNR.js → erb-CObSr7xt.js} +2 -2
  21. package/dist/client/assets/{fileCommentAnnotations-D0gz9Nvl.js → fileCommentAnnotations-BQPQAflx.js} +2 -2
  22. package/dist/client/assets/{git-rebase-D7H5ArnN.js → git-rebase-D-Kfui1X.js} +2 -2
  23. package/dist/client/assets/{glimmer-js-DOuFsyYy.js → glimmer-js-Bj7zsPRR.js} +2 -2
  24. package/dist/client/assets/{glimmer-ts-C_dgMrVH.js → glimmer-ts-BsEDRgeG.js} +2 -2
  25. package/dist/client/assets/{glsl-BJU0wvLz.js → glsl-D98j42v3.js} +2 -2
  26. package/dist/client/assets/{graphql-CXUg7dVm.js → graphql-B1Bl3yQr.js} +2 -2
  27. package/dist/client/assets/{hack-DEwTpBBH.js → hack-C5SdJhmY.js} +2 -2
  28. package/dist/client/assets/{haml-Br-Q1jfj.js → haml-fOHEdRRk.js} +2 -2
  29. package/dist/client/assets/{handlebars-U-qCMjLv.js → handlebars-BprpyHwi.js} +2 -2
  30. package/dist/client/assets/{html-CBhkruiu.js → html-DY94fJQ8.js} +2 -2
  31. package/dist/client/assets/{html-derivative-CtOWonuc.js → html-derivative-BijLMkmB.js} +2 -2
  32. package/dist/client/assets/{http-B-anb7K2.js → http-DgLF_F-Y.js} +2 -2
  33. package/dist/client/assets/{hurl-UbSvk6zv.js → hurl-Lf_gtANJ.js} +2 -2
  34. package/dist/client/assets/index-BWiBk__A.js +660 -0
  35. package/dist/client/assets/index-nkPMSYxq.css +1 -0
  36. package/dist/client/assets/{java-d4Ae9Kq8.js → java-CvbaU0rW.js} +2 -2
  37. package/dist/client/assets/{javascript-DENutVTd.js → javascript-Y_90CTbK.js} +2 -2
  38. package/dist/client/assets/{jinja-Bugh_6jh.js → jinja-Dz3cSyss.js} +2 -2
  39. package/dist/client/assets/{jison-5v-Z1oHG.js → jison-Cy5HwlPH.js} +2 -2
  40. package/dist/client/assets/{json-BhFmIQh2.js → json-BX9adxsl.js} +2 -2
  41. package/dist/client/assets/{jsx-ByAWu7n0.js → jsx-uMzR21MU.js} +2 -2
  42. package/dist/client/assets/{julia-DE4JKJ_I.js → julia-BfKnOvts.js} +2 -2
  43. package/dist/client/assets/{just-Ck-bQzBW.js → just-gPqomEDo.js} +2 -2
  44. package/dist/client/assets/{latex-0aopGn_u.js → latex-HAmg84zi.js} +2 -2
  45. package/dist/client/assets/{liquid-BOV6YY-4.js → liquid-CAfKVpt2.js} +2 -2
  46. package/dist/client/assets/{lua-BS79SG5v.js → lua-MJ-soRMY.js} +2 -2
  47. package/dist/client/assets/{marko-CxCHVwvS.js → marko-Bk2APM9P.js} +2 -2
  48. package/dist/client/assets/{mdc-D8ArMX_3.js → mdc-D-ETA9xK.js} +2 -2
  49. package/dist/client/assets/{nginx-54lPKP4l.js → nginx-NwHNwVvi.js} +2 -2
  50. package/dist/client/assets/{nim-CQW1n-W6.js → nim-uboMGtnG.js} +2 -2
  51. package/dist/client/assets/{perl-TGEf9BSy.js → perl-DZO_qrRW.js} +2 -2
  52. package/dist/client/assets/{php-CRSs0ljw.js → php-BudgHX29.js} +2 -2
  53. package/dist/client/assets/{pug-B94qB-T3.js → pug-1FhoiZN2.js} +2 -2
  54. package/dist/client/assets/{qml-Ce-jpaqY.js → qml-B6ycgh-7.js} +2 -2
  55. package/dist/client/assets/{r-Di-hq60i.js → r-M2HClZBy.js} +2 -2
  56. package/dist/client/assets/{razor-VXC_c_VY.js → razor-EiO_nOGU.js} +2 -2
  57. package/dist/client/assets/{regexp-DCuWLst8.js → regexp-BF12zF9R.js} +2 -2
  58. package/dist/client/assets/{rst-CaleKCxt.js → rst-BW8dKb0T.js} +2 -2
  59. package/dist/client/assets/{ruby-BNvjj0Xx.js → ruby-BcJiTtn_.js} +2 -2
  60. package/dist/client/assets/{sas-CN3Jyhxd.js → sas--CMYy3z3.js} +2 -2
  61. package/dist/client/assets/{scss-iGZoIRyN.js → scss-CBaO992p.js} +2 -2
  62. package/dist/client/assets/{shellscript-B3Nk58br.js → shellscript-IGSv-HCQ.js} +2 -2
  63. package/dist/client/assets/{shellsession-aV0EL5Me.js → shellsession-tY9I_iRt.js} +2 -2
  64. package/dist/client/assets/{soy-CK-MnjGK.js → soy-BvSCSsmo.js} +2 -2
  65. package/dist/client/assets/{sql-WiqY-pg6.js → sql-BuK6MJNF.js} +2 -2
  66. package/dist/client/assets/{stata-L2KCRIsj.js → stata-VS9vYyft.js} +2 -2
  67. package/dist/client/assets/{surrealql-y8rFKpHp.js → surrealql-D4pAe4SW.js} +2 -2
  68. package/dist/client/assets/{svelte-BUjzGJjC.js → svelte-05AEGqMV.js} +2 -2
  69. package/dist/client/assets/{templ-Lwa6_2Pi.js → templ-amlaVpnD.js} +2 -2
  70. package/dist/client/assets/{tex-C6_2o8N6.js → tex-B8w-z73a.js} +2 -2
  71. package/dist/client/assets/textarea-sGc0UkUx.js +46 -0
  72. package/dist/client/assets/{ts-tags-DHGXrdlY.js → ts-tags-CWOgRloL.js} +2 -2
  73. package/dist/client/assets/{tsx-D7a2s_rX.js → tsx-DcSMCm_G.js} +2 -2
  74. package/dist/client/assets/{twig-rodlH6R_.js → twig-BoFkWRc3.js} +2 -2
  75. package/dist/client/assets/{typescript-BSVnlc3E.js → typescript-CnosOGRY.js} +2 -2
  76. package/dist/client/assets/{vue-CPqhBG4f.js → vue-BkQYyvUc.js} +2 -2
  77. package/dist/client/assets/{vue-html-DlcYUp9j.js → vue-html-DQkUheyK.js} +2 -2
  78. package/dist/client/assets/{vue-vine-C33vB5eU.js → vue-vine-PP4GiTEY.js} +2 -2
  79. package/dist/client/assets/{xml-Dk7HbObH.js → xml-B3yPYALE.js} +2 -2
  80. package/dist/client/assets/{xsl-Ce_0ufBe.js → xsl-Cgr4RWex.js} +2 -2
  81. package/dist/client/assets/{yaml-hWCzaJrB.js → yaml-B7fPykX3.js} +2 -2
  82. package/dist/client/index.html +3 -3
  83. package/package.json +1 -1
  84. package/dist/client/assets/arrow-right-qNd2lFlZ.js +0 -2
  85. package/dist/client/assets/index-CADZUNMb.js +0 -664
  86. package/dist/client/assets/index-CPnR_Tju.css +0 -1
  87. package/dist/client/assets/textarea-C26NLrE0.js +0 -46
package/dist/bin.mjs CHANGED
@@ -233,7 +233,7 @@ const make$73 = () => {
233
233
  const layer$69 = Layer.sync(NetService, make$73);
234
234
  //#endregion
235
235
  //#region package.json
236
- var version = "0.1.9";
236
+ var version = "0.1.11";
237
237
  //#endregion
238
238
  //#region src/config.ts
239
239
  /**
@@ -5557,8 +5557,6 @@ const ClientSettingsSchema = Schema$1.Struct({
5557
5557
  sidebarProjectSortOrder: SidebarProjectSortOrder.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_SORT_ORDER))),
5558
5558
  sidebarThreadSortOrder: SidebarThreadSortOrder.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER))),
5559
5559
  sidebarThreadPreviewCount: SidebarThreadPreviewCount.pipe(Schema$1.withDecodingDefault(Effect.succeed(6))),
5560
- sidebarV2Enabled: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(false))),
5561
- sidebarV2ConfiguredByUser: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(false))),
5562
5560
  timestampFormat: TimestampFormat.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_TIMESTAMP_FORMAT))),
5563
5561
  wordWrap: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true)))
5564
5562
  });
@@ -5905,8 +5903,6 @@ Schema$1.Struct({
5905
5903
  sidebarProjectSortOrder: Schema$1.optionalKey(SidebarProjectSortOrder),
5906
5904
  sidebarThreadSortOrder: Schema$1.optionalKey(SidebarThreadSortOrder),
5907
5905
  sidebarThreadPreviewCount: Schema$1.optionalKey(SidebarThreadPreviewCount),
5908
- sidebarV2Enabled: Schema$1.optionalKey(Schema$1.Boolean),
5909
- sidebarV2ConfiguredByUser: Schema$1.optionalKey(Schema$1.Boolean),
5910
5906
  timestampFormat: Schema$1.optionalKey(TimestampFormat),
5911
5907
  wordWrap: Schema$1.optionalKey(Schema$1.Boolean)
5912
5908
  });
@@ -7542,6 +7538,84 @@ var ProjectWriteFileError = class extends Schema$1.TaggedErrorClass()("ProjectWr
7542
7538
  }
7543
7539
  };
7544
7540
  //#endregion
7541
+ //#region ../../packages/contracts/src/threadControl.ts
7542
+ /**
7543
+ * Thread control over MCP - the self-scoped toolkit an agent uses to act on
7544
+ * the thread it is running in.
7545
+ *
7546
+ * Deliberately narrow. The reported need was an agent renaming its own thread;
7547
+ * that needs no cross-thread reach, and the per-session bearer token is the
7548
+ * only thing guarding `/mcp` on a remote-reachable server, so a capability
7549
+ * that could rename arbitrary threads would be a capability to vandalise the
7550
+ * workspace from one leaked token. Delete and create are withheld outright,
7551
+ * the same answer the task toolkit gave. Settle is absent for a structural
7552
+ * reason rather than a policy one: the decider refuses `thread.settle` while
7553
+ * the thread's session is starting or running, and a self-scoped MCP call
7554
+ * always arrives from its own live session, so the call could never succeed.
7555
+ *
7556
+ * Quick-append to memory lives here too: it is the other thing an agent wants
7557
+ * to do *from* a thread, and it shares the toolkit's capability and scope.
7558
+ *
7559
+ * @module contracts/threadControl
7560
+ */
7561
+ const ThreadRenameInput = Schema$1.Struct({ title: TrimmedNonEmptyString.annotate({ description: "The new thread title. Replaces the current one." }) });
7562
+ const ThreadRenameResult = Schema$1.Struct({
7563
+ threadId: ThreadId,
7564
+ title: TrimmedNonEmptyString
7565
+ });
7566
+ /**
7567
+ * Which memory file an append lands in.
7568
+ *
7569
+ * A closed union rather than a filename, because the target directory also
7570
+ * holds credentials and per-machine settings - the same reason the hub sync's
7571
+ * memory allowlist exists. These two names must stay within that allowlist
7572
+ * (`MEMORY_FILE_NAMES` in `sync/memoryFile.ts`), so an appended file is one
7573
+ * the sync will carry to the user's other machines rather than refuse.
7574
+ */
7575
+ const MemoryAppendTarget = Schema$1.Literals(["CLAUDE.md", "AGENTS.md"]);
7576
+ const MemoryAppendInput = Schema$1.Struct({
7577
+ text: TrimmedNonEmptyString.annotate({ description: "Markdown to append to the user-scope memory file, as its own block." }),
7578
+ file: Schema$1.optional(MemoryAppendTarget.annotate({ description: "Which memory file to append to. Defaults to CLAUDE.md." }))
7579
+ });
7580
+ const MemoryAppendResult = Schema$1.Struct({
7581
+ file: MemoryAppendTarget,
7582
+ /** Absolute path the append landed in, so the agent can cite or inspect it. */
7583
+ path: Schema$1.String
7584
+ });
7585
+ var ThreadToolUnavailableError = class extends Schema$1.TaggedErrorClass()("ThreadToolUnavailableError", {
7586
+ capability: Schema$1.Literal("threads"),
7587
+ environmentId: EnvironmentId,
7588
+ threadId: ThreadId,
7589
+ providerSessionId: TrimmedNonEmptyString,
7590
+ providerInstanceId: ProviderInstanceId
7591
+ }) {
7592
+ get message() {
7593
+ return `MCP credential does not grant the ${this.capability} capability.`;
7594
+ }
7595
+ };
7596
+ /** The orchestration engine declined or failed the rename command. */
7597
+ var ThreadRenameRejectedError = class extends Schema$1.TaggedErrorClass()("ThreadRenameRejectedError", {
7598
+ threadId: ThreadId,
7599
+ detail: Schema$1.String
7600
+ }) {
7601
+ get message() {
7602
+ return `Renaming thread ${this.threadId} failed: ${this.detail}`;
7603
+ }
7604
+ };
7605
+ var MemoryAppendFailedError = class extends Schema$1.TaggedErrorClass()("MemoryAppendFailedError", {
7606
+ file: MemoryAppendTarget,
7607
+ detail: Schema$1.String
7608
+ }) {
7609
+ get message() {
7610
+ return `Appending to ${this.file} failed: ${this.detail}`;
7611
+ }
7612
+ };
7613
+ const ThreadControlToolError = Schema$1.Union([
7614
+ ThreadToolUnavailableError,
7615
+ ThreadRenameRejectedError,
7616
+ MemoryAppendFailedError
7617
+ ]);
7618
+ //#endregion
7545
7619
  //#region ../../packages/contracts/src/agentAsset.ts
7546
7620
  /**
7547
7621
  * User-scope agent assets — the things that make an agent behave the same on
@@ -28990,414 +29064,1060 @@ const staticAndDevRouteLayer = HttpRouter.add("GET", "*", Effect.gen(function* (
28990
29064
  contentType
28991
29065
  });
28992
29066
  }));
28993
- //#endregion
28994
- //#region src/checkpointing/Errors.ts
28995
- const CheckpointDiffOperation = Schema$1.Literals(["CheckpointDiffQuery.getTurnDiff", "CheckpointDiffQuery.getFullThreadDiff"]);
28996
- /** The computed result does not satisfy the checkpoint RPC contract. */
28997
- var CheckpointDiffResultInvalidError = class extends Schema$1.TaggedErrorClass()("CheckpointDiffResultInvalidError", {
28998
- operation: CheckpointDiffOperation,
28999
- threadId: ThreadId
29000
- }) {
29001
- get message() {
29002
- const result = this.operation === "CheckpointDiffQuery.getTurnDiff" ? "turn diff" : "full thread diff";
29003
- return `Checkpoint invariant violation in ${this.operation}: Computed ${result} result does not satisfy contract schema.`;
29004
- }
29005
- };
29006
- /** Projection state no longer contains the requested checkpoint thread. */
29007
- var CheckpointThreadNotFoundError = class extends Schema$1.TaggedErrorClass()("CheckpointThreadNotFoundError", {
29008
- operation: CheckpointDiffOperation,
29009
- threadId: ThreadId
29010
- }) {
29011
- get message() {
29012
- return `Checkpoint invariant violation in ${this.operation}: Thread '${this.threadId}' not found.`;
29013
- }
29014
- };
29015
- /** The checkpoint thread has no workspace path from which to compute a diff. */
29016
- var CheckpointWorkspacePathMissingError = class extends Schema$1.TaggedErrorClass()("CheckpointWorkspacePathMissingError", {
29017
- operation: CheckpointDiffOperation,
29018
- threadId: ThreadId
29019
- }) {
29020
- get message() {
29021
- const diff = this.operation === "CheckpointDiffQuery.getTurnDiff" ? "turn diff" : "full thread diff";
29022
- return `Checkpoint invariant violation in ${this.operation}: Workspace path missing for thread '${this.threadId}' when computing ${diff}.`;
29023
- }
29024
- };
29025
- /** The requested turn lies beyond the latest available checkpoint. */
29026
- var CheckpointTurnRangeUnavailableError = class extends Schema$1.TaggedErrorClass()("CheckpointTurnRangeUnavailableError", {
29027
- operation: CheckpointDiffOperation,
29028
- threadId: ThreadId,
29029
- requestedTurnCount: NonNegativeInt,
29030
- availableTurnCount: NonNegativeInt
29031
- }) {
29032
- get message() {
29033
- return `Checkpoint unavailable for thread ${this.threadId} turn ${this.requestedTurnCount}: Turn diff range exceeds current turn count: requested ${this.requestedTurnCount}, current ${this.availableTurnCount}.`;
29034
- }
29035
- };
29036
- /** Expected checkpoint metadata does not contain the requested Git ref. */
29037
- var CheckpointRefUnavailableError = class extends Schema$1.TaggedErrorClass()("CheckpointRefUnavailableError", {
29038
- operation: CheckpointDiffOperation,
29039
- threadId: ThreadId,
29040
- turnCount: NonNegativeInt,
29041
- checkpoint: Schema$1.Literals(["from", "to"])
29042
- }) {
29043
- get message() {
29044
- return `Checkpoint unavailable for thread ${this.threadId} turn ${this.turnCount}: Checkpoint ref is unavailable for turn ${this.turnCount}.`;
29067
+ (() => {
29068
+ try {
29069
+ return process.env.NODE_ENV === "development";
29070
+ } catch {
29071
+ return false;
29045
29072
  }
29046
- };
29047
- //#endregion
29048
- //#region src/checkpointing/Utils.ts
29049
- const CHECKPOINT_REFS_PREFIX = "refs/p4/checkpoints";
29050
- function checkpointRefForThreadTurn(threadId, turnCount) {
29051
- return CheckpointRef.make(`${CHECKPOINT_REFS_PREFIX}/${Encoding.encodeBase64Url(threadId)}/turn/${turnCount}`);
29052
- }
29053
- function resolveThreadWorkspaceCwd(input) {
29054
- const worktreeCwd = input.thread.worktreePath ?? void 0;
29055
- if (worktreeCwd) return worktreeCwd;
29056
- return input.projects.find((project) => project.id === input.thread.projectId)?.workspaceRoot;
29057
- }
29073
+ })();
29074
+ const COMMIT_METADATA_SPLIT = /(?=^From [a-f0-9]+ .+$)/m;
29075
+ const GIT_DIFF_FILE_BREAK_REGEX = /(?=^diff --git)/gm;
29076
+ const FILENAME_HEADER_REGEX = /^(---|\+\+\+)\s+([^\t\r\n]+)/;
29077
+ const FILENAME_HEADER_REGEX_GIT = /^(---|\+\+\+)\s+[ab]\/([^\t\r\n]+)/;
29078
+ const ALTERNATE_FILE_NAMES_GIT = /^diff --git (?:"a\/(.+?)"|a\/(.+?)) (?:"b\/(.+?)"|b\/(.+?))$/;
29079
+ const INDEX_LINE_METADATA = /^index ([0-9a-f]+)\.\.([0-9a-f]+)(?: (\d+))?$/i;
29080
+ Object.freeze({
29081
+ fromStart: 0,
29082
+ fromEnd: 0
29083
+ });
29058
29084
  //#endregion
29059
- //#region src/git/remoteRefs.ts
29060
- function parseRemoteNamesInGitOrder(stdout) {
29061
- const remoteNames = [];
29062
- for (const line of stdout.split("\n")) {
29063
- const remoteName = line.trim();
29064
- if (remoteName.length > 0) remoteNames.push(remoteName);
29065
- }
29066
- return remoteNames;
29067
- }
29068
- function parseRemoteNames(stdout) {
29069
- return parseRemoteNamesInGitOrder(stdout).toSorted((a, b) => b.length - a.length);
29070
- }
29071
- function parseRemoteRefWithRemoteNames(ref, remoteNames) {
29072
- const trimmedRef = ref.trim();
29073
- if (trimmedRef.length === 0) return null;
29074
- for (const remoteName of remoteNames) {
29075
- const remotePrefix = `${remoteName}/`;
29076
- if (!trimmedRef.startsWith(remotePrefix)) continue;
29077
- const branchName = trimmedRef.slice(remotePrefix.length).trim();
29078
- if (branchName.length === 0) return null;
29079
- return {
29080
- remoteRef: trimmedRef,
29081
- remoteName,
29082
- branchName
29083
- };
29085
+ //#region ../../node_modules/.pnpm/@pierre+diffs@1.3.0-beta.5_patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b3_3b51331382fbad9d080c5033ee1e2a6a/node_modules/@pierre/diffs/dist/utils/cleanLastNewline.js
29086
+ function cleanLastNewline(contents) {
29087
+ let end = contents.length;
29088
+ if (contents.charCodeAt(end - 1) === 10) {
29089
+ end--;
29090
+ if (contents.charCodeAt(end - 1) === 13) end--;
29084
29091
  }
29085
- return null;
29086
- }
29087
- function extractBranchNameFromRemoteRef(ref, options) {
29088
- const normalized = ref.trim();
29089
- if (normalized.length === 0) return "";
29090
- if (normalized.startsWith("refs/remotes/")) return extractBranchNameFromRemoteRef(normalized.slice(13), options);
29091
- const parsedRemoteRef = parseRemoteRefWithRemoteNames(normalized, options?.remoteName ? [options.remoteName] : options?.remoteNames ?? []);
29092
- if (parsedRemoteRef) return parsedRemoteRef.branchName;
29093
- const firstSlash = normalized.indexOf("/");
29094
- if (firstSlash === -1) return normalized;
29095
- return normalized.slice(firstSlash + 1).trim();
29092
+ return contents.slice(0, end);
29096
29093
  }
29097
29094
  //#endregion
29098
- //#region src/vcs/GitVcsDriverCore.ts
29099
- const DEFAULT_TIMEOUT_MS$5 = 3e4;
29100
- const DEFAULT_MAX_OUTPUT_BYTES$1 = 1e6;
29101
- const OUTPUT_TRUNCATED_MARKER$1 = "\n\n[truncated]";
29102
- const PREPARED_COMMIT_PATCH_MAX_OUTPUT_BYTES = 49e3;
29103
- const RANGE_COMMIT_SUMMARY_MAX_OUTPUT_BYTES = 19e3;
29104
- const RANGE_DIFF_SUMMARY_MAX_OUTPUT_BYTES = 19e3;
29105
- const RANGE_DIFF_PATCH_MAX_OUTPUT_BYTES = 59e3;
29106
- const REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 12e4;
29107
- const REVIEW_UNTRACKED_DIFF_MAX_OUTPUT_BYTES = 8e4;
29108
- const WORKSPACE_FILES_MAX_OUTPUT_BYTES$1 = 12e4;
29109
- const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15);
29110
- const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5);
29111
- const STATUS_UPSTREAM_REFRESH_FAILURE_BASE_COOLDOWN = Duration.seconds(30);
29112
- const STATUS_UPSTREAM_REFRESH_FAILURE_MAX_COOLDOWN = Duration.minutes(15);
29113
- const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2048;
29114
- const REPOSITORY_PATHS_CACHE_CAPACITY = 2048;
29115
- const REPOSITORY_PATHS_CACHE_TTL = Duration.minutes(10);
29116
- const REPOSITORY_PATHS_REFRESH_COALESCE_TTL = Duration.seconds(5);
29117
- const NON_REPOSITORY_PATHS_CACHE_TTL = Duration.seconds(1);
29118
- const LIST_REFS_SNAPSHOT_CACHE_CAPACITY = 64;
29119
- const LIST_REFS_SNAPSHOT_CACHE_TTL = Duration.minutes(2);
29120
- const LIST_REFS_REFRESH_COALESCE_TTL = Duration.seconds(5);
29121
- const LIST_REFS_REFRESH_FAILURE_COOLDOWN = Duration.seconds(30);
29122
- const STATUS_UPSTREAM_REFRESH_ENV = Object.freeze({
29123
- GCM_INTERACTIVE: "never",
29124
- GIT_ASKPASS: "",
29125
- GIT_TERMINAL_PROMPT: "0",
29126
- SSH_ASKPASS: "",
29127
- SSH_ASKPASS_REQUIRE: "never"
29128
- });
29129
- const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"];
29130
- const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100;
29131
- const NON_REPOSITORY_STATUS_DETAILS = Object.freeze({
29132
- isRepo: false,
29133
- hasOriginRemote: false,
29134
- isDefaultBranch: false,
29135
- branch: null,
29136
- upstreamRef: null,
29137
- hasWorkingTreeChanges: false,
29138
- workingTree: {
29139
- files: [],
29140
- insertions: 0,
29141
- deletions: 0
29142
- },
29143
- hasUpstream: false,
29144
- aheadCount: 0,
29145
- behindCount: 0,
29146
- aheadOfDefaultCount: 0
29147
- });
29148
- const NON_REPOSITORY_REMOTE_STATUS_DETAILS = Object.freeze({
29149
- isRepo: false,
29150
- isDefaultBranch: false,
29151
- branch: null,
29152
- upstreamRef: null,
29153
- hasUpstream: false,
29154
- aheadCount: 0,
29155
- behindCount: 0,
29156
- aheadOfDefaultCount: 0
29157
- });
29158
- var StatusRemoteRefreshCacheKey = class extends Data.Class {};
29159
- function statusUpstreamRefreshFailureCooldown(consecutiveFailures) {
29160
- const exponent = Math.max(0, consecutiveFailures - 1);
29161
- const cooldownMs = Duration.toMillis(STATUS_UPSTREAM_REFRESH_FAILURE_BASE_COOLDOWN) * Math.pow(2, exponent);
29162
- return Duration.min(Duration.millis(cooldownMs), STATUS_UPSTREAM_REFRESH_FAILURE_MAX_COOLDOWN);
29163
- }
29164
- var GitRefsSnapshotCacheKey = class extends Data.Class {};
29165
- var GitRefsRefreshCacheKey = class extends Data.Class {};
29166
- function parseBranchAb(value) {
29167
- const match = value.match(/^\+(\d+)\s+-(\d+)$/);
29168
- if (!match) return {
29169
- ahead: 0,
29170
- behind: 0
29171
- };
29172
- return {
29173
- ahead: Number(match[1] ?? "0"),
29174
- behind: Number(match[2] ?? "0")
29175
- };
29176
- }
29177
- function parseNumstatEntries(stdout) {
29178
- const entries = [];
29179
- for (const line of stdout.split(/\r?\n/g)) {
29180
- if (line.trim().length === 0) continue;
29181
- const [addedRaw, deletedRaw, ...pathParts] = line.split(" ");
29182
- const rawPath = pathParts.length > 1 ? (pathParts.at(-1) ?? "").trim() : pathParts.join(" ").trim();
29183
- if (rawPath.length === 0) continue;
29184
- const added = Number.parseInt(addedRaw ?? "0", 10);
29185
- const deleted = Number.parseInt(deletedRaw ?? "0", 10);
29186
- const renameArrowIndex = rawPath.indexOf(" => ");
29187
- const normalizedPath = renameArrowIndex >= 0 ? rawPath.slice(renameArrowIndex + 4).trim() : rawPath;
29188
- entries.push({
29189
- path: normalizedPath.length > 0 ? normalizedPath : rawPath,
29190
- insertions: Number.isFinite(added) ? added : 0,
29191
- deletions: Number.isFinite(deleted) ? deleted : 0
29192
- });
29193
- }
29194
- return entries;
29195
- }
29196
- function parsePorcelainPath(line) {
29197
- if (line.startsWith("? ") || line.startsWith("! ")) {
29198
- const simple = line.slice(2).trim();
29199
- return simple.length > 0 ? simple : null;
29200
- }
29201
- if (!(line.startsWith("1 ") || line.startsWith("2 ") || line.startsWith("u "))) return null;
29202
- const tabIndex = line.indexOf(" ");
29203
- if (tabIndex >= 0) {
29204
- const [filePath] = line.slice(tabIndex + 1).split(" ");
29205
- return filePath?.trim().length ? filePath.trim() : null;
29206
- }
29207
- const filePath = line.trim().split(/\s+/g).at(-1) ?? "";
29208
- return filePath.length > 0 ? filePath : null;
29209
- }
29210
- function filterBranchesForListQuery(refs, query) {
29211
- if (!query) return refs;
29212
- const normalizedQuery = query.toLowerCase();
29213
- return refs.filter((refName) => refName.name.toLowerCase().includes(normalizedQuery));
29214
- }
29215
- function paginateBranches(input) {
29216
- const cursor = input.cursor ?? 0;
29217
- const limit = input.limit ?? GIT_LIST_BRANCHES_DEFAULT_LIMIT;
29218
- const totalCount = input.refs.length;
29219
- const refs = input.refs.slice(cursor, cursor + limit);
29220
- return {
29221
- refs,
29222
- nextCursor: cursor + refs.length < totalCount ? cursor + refs.length : null,
29223
- totalCount
29224
- };
29225
- }
29226
- function parseWorktreeBranchPaths(stdout) {
29227
- const worktreePaths = /* @__PURE__ */ new Map();
29228
- let currentPath = null;
29229
- let currentBranch = null;
29230
- let currentPrunable = false;
29231
- const flush = () => {
29232
- if (currentPath !== null && currentBranch !== null && !currentPrunable) worktreePaths.set(currentBranch, currentPath);
29233
- currentPath = null;
29234
- currentBranch = null;
29235
- currentPrunable = false;
29236
- };
29237
- for (const field of stdout.split("\0")) if (field === "") flush();
29238
- else if (field.startsWith("worktree ")) currentPath = field.slice(9);
29239
- else if (field.startsWith("branch refs/heads/")) currentBranch = field.slice(18);
29240
- else if (field === "prunable" || field.startsWith("prunable ")) currentPrunable = true;
29241
- flush();
29242
- return worktreePaths;
29243
- }
29244
- function splitNullSeparatedPaths$1(input, truncated) {
29245
- const parts = input.split("\0");
29246
- if (parts.length === 0) return [];
29247
- if (truncated && parts[parts.length - 1]?.length) parts.pop();
29248
- return parts.filter((value) => value.length > 0);
29249
- }
29250
- function splitNullSeparatedGitStdoutPaths(result) {
29251
- return splitNullSeparatedPaths$1(result.stdout, result.stdoutTruncated);
29095
+ //#region ../../node_modules/.pnpm/@pierre+diffs@1.3.0-beta.5_patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b3_3b51331382fbad9d080c5033ee1e2a6a/node_modules/@pierre/diffs/dist/utils/detachString.js
29096
+ const stringDetachEncoder = new TextEncoder();
29097
+ const stringDetachDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
29098
+ const SURROGATE_CODE_UNIT_PATTERN = /[\uD800-\uDFFF]/;
29099
+ const STRING_DETACH_INITIAL_BUFFER_SIZE = 1024;
29100
+ let stringDetachBuffer = new Uint8Array(STRING_DETACH_INITIAL_BUFFER_SIZE);
29101
+ function releaseStringDetachBuffer() {
29102
+ if (stringDetachBuffer.length !== STRING_DETACH_INITIAL_BUFFER_SIZE) stringDetachBuffer = new Uint8Array(STRING_DETACH_INITIAL_BUFFER_SIZE);
29252
29103
  }
29253
- function sanitizeRemoteName(value) {
29254
- const sanitized = value.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
29255
- return sanitized.length > 0 ? sanitized : "fork";
29104
+ function detachString(value) {
29105
+ if (value.length === 0) return value;
29106
+ if (SURROGATE_CODE_UNIT_PATTERN.test(value)) return JSON.parse(JSON.stringify(value));
29107
+ const requiredByteLength = value.length * 3;
29108
+ if (stringDetachBuffer.length < requiredByteLength) stringDetachBuffer = new Uint8Array(requiredByteLength);
29109
+ const { written } = stringDetachEncoder.encodeInto(value, stringDetachBuffer);
29110
+ return stringDetachDecoder.decode(stringDetachBuffer.subarray(0, written));
29256
29111
  }
29257
- function parseRemoteFetchUrls(stdout) {
29258
- const remotes = /* @__PURE__ */ new Map();
29259
- for (const line of stdout.split("\n")) {
29260
- const trimmed = line.trim();
29261
- if (trimmed.length === 0) continue;
29262
- const match = /^(\S+)\s+(\S+)\s+\((fetch|push)\)$/.exec(trimmed);
29263
- if (!match) continue;
29264
- const [, remoteName = "", remoteUrl = "", direction = ""] = match;
29265
- if (direction !== "fetch" || remoteName.length === 0 || remoteUrl.length === 0) continue;
29266
- remotes.set(remoteName, remoteUrl);
29112
+ //#endregion
29113
+ //#region ../../node_modules/.pnpm/@pierre+diffs@1.3.0-beta.5_patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b3_3b51331382fbad9d080c5033ee1e2a6a/node_modules/@pierre/diffs/dist/utils/parsePatchFiles.js
29114
+ function processPatch(data, cacheKeyPrefix, throwOnError) {
29115
+ try {
29116
+ return _processPatch(data, cacheKeyPrefix, throwOnError);
29117
+ } finally {
29118
+ releaseStringDetachBuffer();
29267
29119
  }
29268
- return remotes;
29269
- }
29270
- function parseUpstreamRefWithRemoteNames(upstreamRef, remoteNames) {
29271
- const parsed = parseRemoteRefWithRemoteNames(upstreamRef, remoteNames);
29272
- if (!parsed) return null;
29273
- return {
29274
- upstreamRef,
29275
- remoteName: parsed.remoteName,
29276
- branchName: parsed.branchName
29277
- };
29278
- }
29279
- function parseUpstreamRefByFirstSeparator(upstreamRef) {
29280
- const separatorIndex = upstreamRef.indexOf("/");
29281
- if (separatorIndex <= 0 || separatorIndex === upstreamRef.length - 1) return null;
29282
- const remoteName = upstreamRef.slice(0, separatorIndex).trim();
29283
- const branchName = upstreamRef.slice(separatorIndex + 1).trim();
29284
- if (remoteName.length === 0 || branchName.length === 0) return null;
29285
- return {
29286
- upstreamRef,
29287
- remoteName,
29288
- branchName
29289
- };
29290
29120
  }
29291
- function parseTrackingBranchByUpstreamRef(stdout, upstreamRef) {
29292
- for (const line of stdout.split("\n")) {
29293
- const trimmedLine = line.trim();
29294
- if (trimmedLine.length === 0) continue;
29295
- const [branchNameRaw, upstreamBranchRaw = ""] = trimmedLine.split(" ");
29296
- const branchName = branchNameRaw?.trim() ?? "";
29297
- const candidateUpstreamRef = upstreamBranchRaw.trim();
29298
- if (branchName.length === 0 || candidateUpstreamRef.length === 0) continue;
29299
- if (candidateUpstreamRef === upstreamRef) return branchName;
29121
+ function _processPatch(data, cacheKeyPrefix, throwOnError = false) {
29122
+ const isGitDiff = isGitDiffPatch(data);
29123
+ const rawFiles = isGitDiff ? splitGitDiffFiles(data) : splitUnifiedDiffFiles(data);
29124
+ let patchMetadata;
29125
+ const files = [];
29126
+ for (const fileOrPatchMetadata of rawFiles) {
29127
+ if (isGitDiff && !GIT_DIFF_FILE_BREAK_REGEX.test(fileOrPatchMetadata)) {
29128
+ if (patchMetadata == null) patchMetadata = detachString(fileOrPatchMetadata);
29129
+ else if (throwOnError) throw Error("parsePatchContent: unknown file blob");
29130
+ else console.error("parsePatchContent: unknown file blob:", fileOrPatchMetadata);
29131
+ continue;
29132
+ } else if (!isGitDiff && !startsWithUnifiedDiffFileHeader(fileOrPatchMetadata)) {
29133
+ if (patchMetadata == null) patchMetadata = detachString(fileOrPatchMetadata);
29134
+ else if (throwOnError) throw Error("parsePatchContent: unknown file blob");
29135
+ else console.error("parsePatchContent: unknown file blob:", fileOrPatchMetadata);
29136
+ continue;
29137
+ }
29138
+ const currentFile = _processFile(fileOrPatchMetadata, {
29139
+ cacheKey: cacheKeyPrefix != null ? `${cacheKeyPrefix}-${files.length}` : void 0,
29140
+ isGitDiff,
29141
+ throwOnError
29142
+ });
29143
+ if (currentFile != null) files.push(currentFile);
29300
29144
  }
29301
- return null;
29302
- }
29303
- function deriveLocalBranchNameFromRemoteRef(branchName) {
29304
- const separatorIndex = branchName.indexOf("/");
29305
- if (separatorIndex <= 0 || separatorIndex === branchName.length - 1) return null;
29306
- const localBranch = branchName.slice(separatorIndex + 1).trim();
29307
- return localBranch.length > 0 ? localBranch : null;
29308
- }
29309
- function gitCommandContext(input) {
29310
29145
  return {
29311
- operation: input.operation,
29312
- command: "git",
29313
- cwd: input.cwd,
29314
- argumentCount: input.args.length
29146
+ patchMetadata,
29147
+ files
29315
29148
  };
29316
29149
  }
29317
- function parseDefaultBranchFromRemoteHeadRef(value, remoteName) {
29318
- const trimmed = value.trim();
29319
- const prefix = `refs/remotes/${remoteName}/`;
29320
- if (!trimmed.startsWith(prefix)) return null;
29321
- const refName = trimmed.slice(prefix.length).trim();
29322
- return refName.length > 0 ? refName : null;
29323
- }
29324
- function isMissingGitCwdError(error) {
29325
- if (!(error.cause instanceof PlatformError.PlatformError)) return false;
29326
- const reason = error.cause.reason;
29327
- if (reason._tag === "NotFound") return reason.pathOrDescriptor === error.cwd;
29328
- return reason._tag === "BadResource" && reason.pathOrDescriptor === error.cwd && typeof reason.cause === "object" && reason.cause !== null && "code" in reason.cause && reason.cause.code === "ENOTDIR";
29329
- }
29330
- function isNonRepositoryGitStderr(stderr) {
29331
- return stderr.toLowerCase().includes("not a git repository");
29332
- }
29333
- const nowUnixNano = DateTime.now.pipe(Effect.map((now) => BigInt(DateTime.toEpochMillis(now)) * 1000000n));
29334
- const addCurrentSpanEvent = (name, attributes) => Effect.gen(function* () {
29335
- const span = yield* Effect.currentSpan;
29336
- const timestamp = yield* nowUnixNano;
29337
- yield* Effect.sync(() => {
29338
- span.event(name, timestamp, compactTraceAttributes(attributes));
29339
- });
29340
- }).pipe(Effect.catchTags({ NoSuchElementError: () => Effect.void }));
29341
- function trace2ChildKey(record) {
29342
- const childId = record.child_id;
29343
- if (typeof childId === "number" || typeof childId === "string") return String(childId);
29344
- const hookName = record.hook_name;
29345
- return typeof hookName === "string" && hookName.trim().length > 0 ? hookName.trim() : null;
29346
- }
29347
- const Trace2Record = Schema$1.Record(Schema$1.String, Schema$1.Unknown);
29348
- const createTrace2Monitor = Effect.fn("createTrace2Monitor")(function* (input, progress) {
29349
- if (!progress?.onHookStarted && !progress?.onHookFinished) return {
29350
- env: {},
29351
- flush: Effect.void
29352
- };
29353
- const fs = yield* FileSystem.FileSystem;
29354
- const path = yield* Path.Path;
29355
- const traceFilePath = yield* fs.makeTempFileScoped({
29356
- prefix: `p4code-git-trace2-${process.pid}-`,
29357
- suffix: ".json"
29358
- });
29359
- const hookStartByChildKey = /* @__PURE__ */ new Map();
29360
- const traceTailState = yield* Ref.make({
29361
- processedChars: 0,
29362
- remainder: ""
29363
- });
29364
- const handleTraceLine = Effect.fn("handleTraceLine")(function* (line) {
29365
- const trimmedLine = line.trim();
29366
- if (trimmedLine.length === 0) return;
29367
- const traceRecord = decodeJsonResult(Trace2Record)(trimmedLine);
29368
- if (Result.isFailure(traceRecord)) {
29369
- yield* Effect.logDebug(`GitVcsDriver.trace2: failed to parse trace line for ${input.operation} in ${input.cwd} (${input.args.length} arguments)`, traceRecord.failure);
29370
- return;
29371
- }
29372
- if (traceRecord.success.child_class !== "hook") return;
29373
- const event = traceRecord.success.event;
29374
- const childKey = trace2ChildKey(traceRecord.success);
29375
- if (childKey === null) return;
29376
- const started = hookStartByChildKey.get(childKey);
29377
- const hookNameFromEvent = typeof traceRecord.success.hook_name === "string" ? traceRecord.success.hook_name.trim() : "";
29378
- const hookName = hookNameFromEvent.length > 0 ? hookNameFromEvent : started?.hookName ?? "";
29379
- if (hookName.length === 0) return;
29380
- if (event === "child_start") {
29381
- const now = yield* DateTime.now;
29382
- hookStartByChildKey.set(childKey, {
29383
- hookName,
29384
- startedAtMs: DateTime.toEpochMillis(now)
29385
- });
29386
- yield* addCurrentSpanEvent("git.hook.started", { hookName });
29387
- if (progress.onHookStarted) yield* progress.onHookStarted(hookName);
29388
- return;
29150
+ function _processFile(fileDiffString, { cacheKey, isGitDiff = GIT_DIFF_FILE_BREAK_REGEX.test(fileDiffString), oldFile, newFile, throwOnError = false } = {}) {
29151
+ let lastHunkEnd = 0;
29152
+ const hunks = splitAtLinePrefix(fileDiffString, "@@ ");
29153
+ let currentFile;
29154
+ const isPartial = oldFile == null || newFile == null;
29155
+ let deletionLineIndex = 0;
29156
+ let additionLineIndex = 0;
29157
+ for (const hunk of hunks) {
29158
+ const lines = splitWithNewlines(hunk);
29159
+ const firstLine = lines[0];
29160
+ if (firstLine == null) {
29161
+ if (throwOnError) throw Error("parsePatchContent: invalid hunk");
29162
+ else console.error("parsePatchContent: invalid hunk", hunk);
29163
+ continue;
29389
29164
  }
29390
- if (event === "child_exit") {
29391
- hookStartByChildKey.delete(childKey);
29392
- const code = traceRecord.success.exitCode;
29393
- const exitCode = typeof code === "number" && Number.isInteger(code) ? code : null;
29394
- const now = yield* DateTime.now;
29395
- const durationMs = started ? Math.max(0, DateTime.toEpochMillis(now) - started.startedAtMs) : null;
29396
- yield* addCurrentSpanEvent("git.hook.finished", {
29397
- hookName: started?.hookName ?? hookName,
29398
- exitCode,
29399
- durationMs
29400
- });
29165
+ const fileHeader = parseHunkHeader(firstLine);
29166
+ let additionLines = 0;
29167
+ let deletionLines = 0;
29168
+ if (fileHeader == null || currentFile == null) {
29169
+ if (currentFile != null) {
29170
+ if (throwOnError) throw Error("parsePatchContent: Invalid hunk");
29171
+ else console.error("parsePatchContent: Invalid hunk", hunk);
29172
+ continue;
29173
+ }
29174
+ currentFile = {
29175
+ name: "",
29176
+ type: "change",
29177
+ hunks: [],
29178
+ splitLineCount: 0,
29179
+ unifiedLineCount: 0,
29180
+ isPartial,
29181
+ additionLines: !isPartial && oldFile != null && newFile != null ? splitFileContents(newFile.contents) : [],
29182
+ deletionLines: !isPartial && oldFile != null && newFile != null ? splitFileContents(oldFile.contents) : [],
29183
+ cacheKey: maybeDetachOptionalString(cacheKey)
29184
+ };
29185
+ if (currentFile.additionLines.length === 1 && newFile?.contents === "") currentFile.additionLines.length = 0;
29186
+ if (currentFile.deletionLines.length === 1 && oldFile?.contents === "") currentFile.deletionLines.length = 0;
29187
+ for (const line of lines) {
29188
+ if (line.startsWith("diff --git")) {
29189
+ const filenameMatch$1 = line.trim().match(ALTERNATE_FILE_NAMES_GIT);
29190
+ const prevName = filenameMatch$1?.[1] ?? filenameMatch$1?.[2];
29191
+ const name = filenameMatch$1?.[3] ?? filenameMatch$1?.[4];
29192
+ if (prevName == null || name == null) {
29193
+ if (throwOnError) throw Error("parsePatchContent: invalid git diff header");
29194
+ else console.error("parsePatchContent: invalid git diff header", line);
29195
+ continue;
29196
+ }
29197
+ currentFile.name = detachString(name.trim());
29198
+ if (prevName !== name) currentFile.prevName = detachString(prevName.trim());
29199
+ continue;
29200
+ }
29201
+ const filenameMatch = line.startsWith("---") || line.startsWith("+++") ? line.match(isGitDiff ? FILENAME_HEADER_REGEX_GIT : FILENAME_HEADER_REGEX) : null;
29202
+ if (filenameMatch != null) {
29203
+ const [, type, fileName] = filenameMatch;
29204
+ if (type === "---" && fileName !== "/dev/null") {
29205
+ const detachedFileName = detachString(fileName.trim());
29206
+ currentFile.prevName = detachedFileName;
29207
+ currentFile.name = detachedFileName;
29208
+ } else if (type === "+++" && fileName !== "/dev/null") currentFile.name = detachString(fileName.trim());
29209
+ } else if (isGitDiff) {
29210
+ if (line.startsWith("new mode ")) currentFile.mode = detachString(line.slice(8).trim());
29211
+ if (line.startsWith("old mode ")) currentFile.prevMode = detachString(line.slice(8).trim());
29212
+ if (line.startsWith("new file mode")) {
29213
+ currentFile.type = "new";
29214
+ currentFile.mode = detachString(line.slice(13).trim());
29215
+ }
29216
+ if (line.startsWith("deleted file mode")) {
29217
+ currentFile.type = "deleted";
29218
+ currentFile.mode = detachString(line.slice(17).trim());
29219
+ }
29220
+ if (line.startsWith("similarity index")) if (line.startsWith("similarity index 100%")) currentFile.type = "rename-pure";
29221
+ else currentFile.type = "rename-changed";
29222
+ if (line.startsWith("index ")) {
29223
+ const [, prevObjectId, newObjectId, mode] = line.trim().match(INDEX_LINE_METADATA) ?? [];
29224
+ if (prevObjectId != null) currentFile.prevObjectId = detachString(prevObjectId);
29225
+ if (newObjectId != null) currentFile.newObjectId = detachString(newObjectId);
29226
+ if (mode != null) currentFile.mode = detachString(mode);
29227
+ }
29228
+ if (line.startsWith("rename from ")) currentFile.prevName = detachString(line.slice(12).trim());
29229
+ if (line.startsWith("rename to ")) currentFile.name = detachString(line.slice(10).trim());
29230
+ }
29231
+ }
29232
+ continue;
29233
+ }
29234
+ let currentContent;
29235
+ let lastLineType;
29236
+ while (lines.length > 0 && (lines[lines.length - 1] === "\n" || lines[lines.length - 1] === "\r" || lines[lines.length - 1] === "\r\n" || lines[lines.length - 1] === "")) lines.pop();
29237
+ const { additionStart, deletionStart } = fileHeader;
29238
+ deletionLineIndex = isPartial ? deletionLineIndex : deletionStart - 1;
29239
+ additionLineIndex = isPartial ? additionLineIndex : additionStart - 1;
29240
+ const hunkData = {
29241
+ collapsedBefore: 0,
29242
+ splitLineCount: 0,
29243
+ splitLineStart: 0,
29244
+ unifiedLineCount: 0,
29245
+ unifiedLineStart: 0,
29246
+ additionCount: fileHeader.additionCount,
29247
+ additionStart,
29248
+ additionLines,
29249
+ deletionCount: fileHeader.deletionCount,
29250
+ deletionStart,
29251
+ deletionLines,
29252
+ deletionLineIndex,
29253
+ additionLineIndex,
29254
+ hunkContent: [],
29255
+ hunkContext: maybeDetachOptionalString(fileHeader.hunkContext),
29256
+ hunkSpecs: detachString(firstLine),
29257
+ noEOFCRAdditions: false,
29258
+ noEOFCRDeletions: false
29259
+ };
29260
+ let parsedAdditionLines = 0;
29261
+ let parsedDeletionLines = 0;
29262
+ for (let lineIndex = 1; lineIndex < lines.length; lineIndex++) {
29263
+ const rawLine = lines[lineIndex];
29264
+ if (parsedAdditionLines >= hunkData.additionCount && parsedDeletionLines >= hunkData.deletionCount && !rawLine.startsWith("\\")) {
29265
+ if (throwOnError && isHunkBodyLine(rawLine) && !isFormatPatchVersionSeparator(rawLine)) throw Error("parsePatchContent: hunk has more lines than expected");
29266
+ break;
29267
+ }
29268
+ const firstChar = rawLine[0];
29269
+ if (firstChar !== "+" && firstChar !== "-" && firstChar !== " " && firstChar !== "\\") {
29270
+ if (throwOnError) throw Error("parsePatchContent: invalid hunk line");
29271
+ console.error(`parseLineType: Invalid firstChar: "${firstChar}", full line: "${rawLine}"`);
29272
+ console.error("processFile: invalid rawLine:", rawLine);
29273
+ continue;
29274
+ }
29275
+ const type = parseRawLineType(firstChar);
29276
+ if (type === "addition") {
29277
+ if (throwOnError && parsedAdditionLines >= hunkData.additionCount) throw Error("parsePatchContent: hunk has too many addition lines");
29278
+ const line = getParsedLineContent(rawLine);
29279
+ if (currentContent == null || currentContent.type !== "change") {
29280
+ currentContent = createContentGroup("change", deletionLineIndex, additionLineIndex);
29281
+ hunkData.hunkContent.push(currentContent);
29282
+ }
29283
+ additionLineIndex++;
29284
+ parsedAdditionLines++;
29285
+ if (isPartial) currentFile.additionLines.push(line);
29286
+ currentContent.additions++;
29287
+ additionLines++;
29288
+ lastLineType = "addition";
29289
+ } else if (type === "deletion") {
29290
+ if (throwOnError && parsedDeletionLines >= hunkData.deletionCount) throw Error("parsePatchContent: hunk has too many deletion lines");
29291
+ const line = getParsedLineContent(rawLine);
29292
+ if (currentContent == null || currentContent.type !== "change") {
29293
+ currentContent = createContentGroup("change", deletionLineIndex, additionLineIndex);
29294
+ hunkData.hunkContent.push(currentContent);
29295
+ }
29296
+ deletionLineIndex++;
29297
+ parsedDeletionLines++;
29298
+ if (isPartial) currentFile.deletionLines.push(line);
29299
+ currentContent.deletions++;
29300
+ deletionLines++;
29301
+ lastLineType = "deletion";
29302
+ } else if (type === "context") {
29303
+ if (throwOnError && (parsedDeletionLines >= hunkData.deletionCount || parsedAdditionLines >= hunkData.additionCount)) throw Error("parsePatchContent: hunk has too many context lines");
29304
+ const line = getParsedLineContent(rawLine);
29305
+ if (currentContent == null || currentContent.type !== "context") {
29306
+ currentContent = createContentGroup("context", deletionLineIndex, additionLineIndex);
29307
+ hunkData.hunkContent.push(currentContent);
29308
+ }
29309
+ additionLineIndex++;
29310
+ deletionLineIndex++;
29311
+ parsedAdditionLines++;
29312
+ parsedDeletionLines++;
29313
+ if (isPartial) {
29314
+ currentFile.deletionLines.push(line);
29315
+ currentFile.additionLines.push(line);
29316
+ }
29317
+ currentContent.lines++;
29318
+ lastLineType = "context";
29319
+ } else if (type === "metadata" && currentContent != null) {
29320
+ if (currentContent.type === "context") {
29321
+ hunkData.noEOFCRAdditions = true;
29322
+ hunkData.noEOFCRDeletions = true;
29323
+ } else if (lastLineType === "deletion") hunkData.noEOFCRDeletions = true;
29324
+ else if (lastLineType === "addition") hunkData.noEOFCRAdditions = true;
29325
+ if (isPartial && (lastLineType === "addition" || lastLineType === "context")) {
29326
+ const lastIndex = currentFile.additionLines.length - 1;
29327
+ if (lastIndex >= 0) currentFile.additionLines[lastIndex] = cleanLastNewline(currentFile.additionLines[lastIndex]);
29328
+ }
29329
+ if (isPartial && (lastLineType === "deletion" || lastLineType === "context")) {
29330
+ const lastIndex = currentFile.deletionLines.length - 1;
29331
+ if (lastIndex >= 0) currentFile.deletionLines[lastIndex] = cleanLastNewline(currentFile.deletionLines[lastIndex]);
29332
+ }
29333
+ }
29334
+ }
29335
+ if (throwOnError && (parsedAdditionLines !== hunkData.additionCount || parsedDeletionLines !== hunkData.deletionCount)) throw Error("parsePatchContent: hunk line count mismatch");
29336
+ hunkData.additionLines = additionLines;
29337
+ hunkData.deletionLines = deletionLines;
29338
+ hunkData.collapsedBefore = Math.max(hunkData.additionStart - 1 - lastHunkEnd, 0);
29339
+ currentFile.hunks.push(hunkData);
29340
+ lastHunkEnd = hunkData.additionStart + hunkData.additionCount - 1;
29341
+ for (const content of hunkData.hunkContent) if (content.type === "context") {
29342
+ hunkData.splitLineCount += content.lines;
29343
+ hunkData.unifiedLineCount += content.lines;
29344
+ } else {
29345
+ hunkData.splitLineCount += Math.max(content.additions, content.deletions);
29346
+ hunkData.unifiedLineCount += content.deletions + content.additions;
29347
+ }
29348
+ hunkData.splitLineStart = currentFile.splitLineCount + hunkData.collapsedBefore;
29349
+ hunkData.unifiedLineStart = currentFile.unifiedLineCount + hunkData.collapsedBefore;
29350
+ currentFile.splitLineCount += hunkData.collapsedBefore + hunkData.splitLineCount;
29351
+ currentFile.unifiedLineCount += hunkData.collapsedBefore + hunkData.unifiedLineCount;
29352
+ }
29353
+ if (currentFile == null) return;
29354
+ if (throwOnError && isPartial && !isGitDiff && currentFile.hunks.length === 0) throw Error("parsePatchContent: unified file has no hunks");
29355
+ if (currentFile.hunks.length > 0 && !isPartial && currentFile.additionLines.length > 0 && currentFile.deletionLines.length > 0) {
29356
+ const lastHunk = currentFile.hunks[currentFile.hunks.length - 1];
29357
+ const lastHunkEnd$1 = lastHunk.additionStart + lastHunk.additionCount - 1;
29358
+ const totalFileLines = currentFile.additionLines.length;
29359
+ const collapsedAfter = Math.max(totalFileLines - lastHunkEnd$1, 0);
29360
+ currentFile.splitLineCount += collapsedAfter;
29361
+ currentFile.unifiedLineCount += collapsedAfter;
29362
+ }
29363
+ if (!isGitDiff) {
29364
+ if (currentFile.prevName != null && currentFile.name !== currentFile.prevName) if (currentFile.hunks.length > 0) currentFile.type = "rename-changed";
29365
+ else currentFile.type = "rename-pure";
29366
+ else if ((oldFile == null || oldFile.contents === "") && newFile != null && newFile.contents !== "") currentFile.type = "new";
29367
+ else if (oldFile != null && oldFile.contents !== "" && (newFile == null || newFile.contents === "")) currentFile.type = "deleted";
29368
+ }
29369
+ if (currentFile.type !== "rename-pure" && currentFile.type !== "rename-changed") currentFile.prevName = void 0;
29370
+ return currentFile;
29371
+ }
29372
+ /**
29373
+ * Parses a patch file string into an array of parsed patches.
29374
+ *
29375
+ * @param data - The raw patch file content (supports multi-commit patches)
29376
+ * @param cacheKeyPrefix - Optional prefix for generating cache keys. When provided,
29377
+ * each file in the patch will get a cache key in the format `prefix-patchIndex-fileIndex`.
29378
+ * This enables caching of rendered diff results in the worker pool.
29379
+ */
29380
+ function parsePatchFiles(data, cacheKeyPrefix, throwOnError = false) {
29381
+ const patches = [];
29382
+ const rawPatches = hasCommitMetadataBoundary(data) ? data.split(COMMIT_METADATA_SPLIT) : [data];
29383
+ for (const patch of rawPatches) try {
29384
+ patches.push(processPatch(patch, cacheKeyPrefix != null ? `${cacheKeyPrefix}-${patches.length}` : void 0, throwOnError));
29385
+ } catch (error) {
29386
+ if (throwOnError) throw error;
29387
+ else console.error(error);
29388
+ }
29389
+ return patches;
29390
+ }
29391
+ function hasCommitMetadataBoundary(data) {
29392
+ return data.startsWith("From ") || data.includes("\nFrom ");
29393
+ }
29394
+ function splitFileContents(contents) {
29395
+ const lines = splitWithNewlines(contents);
29396
+ for (let index = 0; index < lines.length; index++) lines[index] = detachString(lines[index]);
29397
+ return lines;
29398
+ }
29399
+ function splitWithNewlines(contents) {
29400
+ if (contents.length === 0) return [""];
29401
+ const lines = [];
29402
+ let startIndex = 0;
29403
+ for (;;) {
29404
+ const newlineIndex = contents.indexOf("\n", startIndex);
29405
+ if (newlineIndex === -1) break;
29406
+ lines.push(contents.slice(startIndex, newlineIndex + 1));
29407
+ startIndex = newlineIndex + 1;
29408
+ }
29409
+ if (startIndex < contents.length) lines.push(contents.slice(startIndex));
29410
+ return lines;
29411
+ }
29412
+ function splitGitDiffFiles(contents) {
29413
+ return splitAtLinePrefix(contents, "diff --git");
29414
+ }
29415
+ function splitUnifiedDiffFiles(contents) {
29416
+ if (contents.length === 0) return [""];
29417
+ const parts = [];
29418
+ let partStartIndex = 0;
29419
+ let lineStartIndex = 0;
29420
+ let remainingDeletionLines = 0;
29421
+ let remainingAdditionLines = 0;
29422
+ let hasOpenedUnifiedFile = false;
29423
+ while (lineStartIndex < contents.length) {
29424
+ const nextLineStartIndex = getNextLineStartIndex(contents, lineStartIndex);
29425
+ if (remainingDeletionLines <= 0 && remainingAdditionLines <= 0) {
29426
+ if (isUnifiedDiffFileHeaderAt(contents, lineStartIndex)) {
29427
+ if (lineStartIndex > partStartIndex) parts.push(contents.slice(partStartIndex, lineStartIndex));
29428
+ partStartIndex = lineStartIndex;
29429
+ hasOpenedUnifiedFile = true;
29430
+ lineStartIndex = getNextLineStartIndex(contents, nextLineStartIndex);
29431
+ continue;
29432
+ }
29433
+ if (hasOpenedUnifiedFile && contents.startsWith("@@ -", lineStartIndex)) {
29434
+ const fileHeader = parseHunkHeader(contents.slice(lineStartIndex, nextLineStartIndex));
29435
+ if (fileHeader != null) {
29436
+ remainingDeletionLines = fileHeader.deletionCount;
29437
+ remainingAdditionLines = fileHeader.additionCount;
29438
+ }
29439
+ }
29440
+ lineStartIndex = nextLineStartIndex;
29441
+ continue;
29442
+ }
29443
+ const firstChar = contents[lineStartIndex];
29444
+ if (firstChar === "\\") {
29445
+ lineStartIndex = nextLineStartIndex;
29446
+ continue;
29447
+ }
29448
+ if (firstChar === " ") {
29449
+ remainingDeletionLines = Math.max(remainingDeletionLines - 1, 0);
29450
+ remainingAdditionLines = Math.max(remainingAdditionLines - 1, 0);
29451
+ } else if (firstChar === "-") remainingDeletionLines = Math.max(remainingDeletionLines - 1, 0);
29452
+ else if (firstChar === "+") remainingAdditionLines = Math.max(remainingAdditionLines - 1, 0);
29453
+ lineStartIndex = nextLineStartIndex;
29454
+ }
29455
+ parts.push(contents.slice(partStartIndex));
29456
+ return parts;
29457
+ }
29458
+ function startsWithUnifiedDiffFileHeader(contents) {
29459
+ return isUnifiedDiffFileHeaderAt(contents, 0);
29460
+ }
29461
+ function isUnifiedDiffFileHeaderAt(contents, lineStartIndex) {
29462
+ const nextLineStartIndex = getNextLineStartIndex(contents, lineStartIndex);
29463
+ return isUnifiedDiffHeaderLineAt(contents, lineStartIndex, "---") && isUnifiedDiffHeaderLineAt(contents, nextLineStartIndex, "+++");
29464
+ }
29465
+ function isUnifiedDiffHeaderLineAt(contents, lineStartIndex, prefix) {
29466
+ if (!contents.startsWith(prefix, lineStartIndex)) return false;
29467
+ const separator = contents[lineStartIndex + prefix.length];
29468
+ if (separator !== " " && separator !== " ") return false;
29469
+ for (let index = lineStartIndex + prefix.length + 1; index < contents.length; index++) {
29470
+ const char = contents[index];
29471
+ if (char === "\n" || char === "\r") break;
29472
+ if (char !== " " && char !== " ") return true;
29473
+ }
29474
+ return false;
29475
+ }
29476
+ function getNextLineStartIndex(contents, lineStartIndex) {
29477
+ const newlineIndex = contents.indexOf("\n", lineStartIndex);
29478
+ return newlineIndex === -1 ? contents.length : newlineIndex + 1;
29479
+ }
29480
+ function isHunkBodyLine(line) {
29481
+ const firstChar = line[0];
29482
+ return firstChar === "+" || firstChar === "-" || firstChar === " ";
29483
+ }
29484
+ function isFormatPatchVersionSeparator(line) {
29485
+ if (!line.startsWith("--")) return false;
29486
+ for (let index = 2; index < line.length; index++) {
29487
+ const char = line[index];
29488
+ if (char !== " " && char !== " " && char !== "\n" && char !== "\r") return false;
29489
+ }
29490
+ return true;
29491
+ }
29492
+ function parseHunkHeader(line) {
29493
+ if (!line.startsWith("@@ -")) return;
29494
+ let index = 4;
29495
+ const deletionStartResult = readPositiveInteger(line, index);
29496
+ if (deletionStartResult == null) return;
29497
+ const deletionStart = deletionStartResult.value;
29498
+ index = deletionStartResult.endIndex;
29499
+ let deletionCount = 1;
29500
+ if (line[index] === ",") {
29501
+ const deletionCountResult = readPositiveInteger(line, index + 1);
29502
+ if (deletionCountResult == null) return;
29503
+ deletionCount = deletionCountResult.value;
29504
+ index = deletionCountResult.endIndex;
29505
+ }
29506
+ if (line[index] !== " " || line[index + 1] !== "+") return;
29507
+ index += 2;
29508
+ const additionStartResult = readPositiveInteger(line, index);
29509
+ if (additionStartResult == null) return;
29510
+ const additionStart = additionStartResult.value;
29511
+ index = additionStartResult.endIndex;
29512
+ let additionCount = 1;
29513
+ if (line[index] === ",") {
29514
+ const additionCountResult = readPositiveInteger(line, index + 1);
29515
+ if (additionCountResult == null) return;
29516
+ additionCount = additionCountResult.value;
29517
+ index = additionCountResult.endIndex;
29518
+ }
29519
+ if (line[index] !== " " || line[index + 1] !== "@" || line[index + 2] !== "@") return;
29520
+ let hunkContext;
29521
+ const contextStartIndex = index + 3;
29522
+ if (line[contextStartIndex] === " ") hunkContext = trimLineEnd(line.slice(contextStartIndex + 1));
29523
+ return {
29524
+ additionCount,
29525
+ additionStart,
29526
+ deletionCount,
29527
+ deletionStart,
29528
+ hunkContext
29529
+ };
29530
+ }
29531
+ function readPositiveInteger(value, startIndex) {
29532
+ let index = startIndex;
29533
+ let parsedValue = 0;
29534
+ for (; index < value.length; index++) {
29535
+ const digit = value.charCodeAt(index) - 48;
29536
+ if (digit < 0 || digit > 9) break;
29537
+ parsedValue = parsedValue * 10 + digit;
29538
+ }
29539
+ if (index === startIndex) return;
29540
+ return {
29541
+ value: parsedValue,
29542
+ endIndex: index
29543
+ };
29544
+ }
29545
+ function trimLineEnd(value) {
29546
+ if (value.endsWith("\r\n")) return value.slice(0, -2);
29547
+ if (value.endsWith("\n")) return value.slice(0, -1);
29548
+ return value;
29549
+ }
29550
+ function isGitDiffPatch(data) {
29551
+ return data.startsWith("diff --git") || data.includes("\ndiff --git");
29552
+ }
29553
+ function splitAtLinePrefix(contents, prefix) {
29554
+ if (contents.length === 0) return [""];
29555
+ const newlinePrefix = `\n${prefix}`;
29556
+ const firstBoundaryIndex = contents.startsWith(prefix) ? 0 : findLinePrefixIndex(contents, newlinePrefix, 0);
29557
+ if (firstBoundaryIndex === -1) return [contents];
29558
+ const parts = [];
29559
+ if (firstBoundaryIndex > 0) parts.push(contents.slice(0, firstBoundaryIndex));
29560
+ let startIndex = firstBoundaryIndex;
29561
+ for (;;) {
29562
+ const nextBoundaryIndex = findLinePrefixIndex(contents, newlinePrefix, startIndex + 1);
29563
+ if (nextBoundaryIndex === -1) break;
29564
+ parts.push(contents.slice(startIndex, nextBoundaryIndex));
29565
+ startIndex = nextBoundaryIndex;
29566
+ }
29567
+ parts.push(contents.slice(startIndex));
29568
+ return parts;
29569
+ }
29570
+ function findLinePrefixIndex(contents, newlinePrefix, fromIndex) {
29571
+ const index = contents.indexOf(newlinePrefix, fromIndex);
29572
+ return index === -1 ? -1 : index + 1;
29573
+ }
29574
+ function maybeDetachOptionalString(value) {
29575
+ return value == null ? value : detachString(value);
29576
+ }
29577
+ function parseRawLineType(firstChar) {
29578
+ return firstChar === " " ? "context" : firstChar === "\\" ? "metadata" : firstChar === "+" ? "addition" : "deletion";
29579
+ }
29580
+ function getParsedLineContent(rawLine) {
29581
+ const processedLine = rawLine.slice(1);
29582
+ return detachString(processedLine === "" ? "\n" : processedLine);
29583
+ }
29584
+ function createContentGroup(type, deletionLineIndex, additionLineIndex) {
29585
+ if (type === "change") return {
29586
+ type: "change",
29587
+ additions: 0,
29588
+ deletions: 0,
29589
+ additionLineIndex,
29590
+ deletionLineIndex
29591
+ };
29592
+ return {
29593
+ type: "context",
29594
+ lines: 0,
29595
+ additionLineIndex,
29596
+ deletionLineIndex
29597
+ };
29598
+ }
29599
+ //#endregion
29600
+ //#region src/checkpointing/ThreadChangedFiles.ts
29601
+ /**
29602
+ * Per-thread changed-file attribution for checkpoint diffs.
29603
+ *
29604
+ * A checkpoint snapshots the whole working tree (`git add -A`), so on a shared
29605
+ * worktree the diff between two of a thread's checkpoints also contains every
29606
+ * edit other threads made in the window. The thread's own tool activities are
29607
+ * per-thread by construction, and the inline changed-files summary the clients
29608
+ * derive from them is already the surface users trust. These helpers extract
29609
+ * that same path list on the server and cut a checkpoint diff down to it.
29610
+ *
29611
+ * `collectActivityChangedFilePaths` must stay in lockstep with
29612
+ * `collectChangedFiles` in `apps/web/src/session-logic.ts` (and the mobile
29613
+ * copy in `threadActivity.ts`): same field names, same nested keys, same depth
29614
+ * bound. The one deliberate difference is dropping the cap of 12 paths - that
29615
+ * cap exists for display, and a filter that stopped collecting would silently
29616
+ * drop real work from the diff.
29617
+ *
29618
+ * @module ThreadChangedFiles
29619
+ */
29620
+ const CHANGED_FILE_FIELDS = [
29621
+ "path",
29622
+ "filePath",
29623
+ "relativePath",
29624
+ "filename",
29625
+ "newPath",
29626
+ "oldPath"
29627
+ ];
29628
+ const NESTED_PAYLOAD_KEYS = [
29629
+ "item",
29630
+ "result",
29631
+ "input",
29632
+ "data",
29633
+ "changes",
29634
+ "files",
29635
+ "edits",
29636
+ "patch",
29637
+ "patches",
29638
+ "operations"
29639
+ ];
29640
+ const MAX_COLLECT_DEPTH = 4;
29641
+ function asRecord$3(value) {
29642
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
29643
+ }
29644
+ function pushChangedFilePath(target, value) {
29645
+ if (typeof value !== "string") return;
29646
+ const normalized = value.trim();
29647
+ if (normalized.length === 0) return;
29648
+ target.add(normalized);
29649
+ }
29650
+ function collectChangedFilePaths(value, target, depth) {
29651
+ if (depth > MAX_COLLECT_DEPTH) return;
29652
+ if (Array.isArray(value)) {
29653
+ for (const entry of value) collectChangedFilePaths(entry, target, depth + 1);
29654
+ return;
29655
+ }
29656
+ const record = asRecord$3(value);
29657
+ if (!record) return;
29658
+ for (const field of CHANGED_FILE_FIELDS) pushChangedFilePath(target, record[field]);
29659
+ for (const nestedKey of NESTED_PAYLOAD_KEYS) if (nestedKey in record) collectChangedFilePaths(record[nestedKey], target, depth + 1);
29660
+ }
29661
+ /**
29662
+ * Extract the file paths a single activity payload names, exactly as the
29663
+ * clients do for the inline changed-files summary.
29664
+ */
29665
+ function collectActivityChangedFilePaths(payload) {
29666
+ const target = /* @__PURE__ */ new Set();
29667
+ collectChangedFilePaths(asRecord$3(asRecord$3(payload)?.data), target, 0);
29668
+ return target;
29669
+ }
29670
+ /**
29671
+ * Normalize activity-named paths to the workspace-relative form git patches
29672
+ * use. Adapters report paths as the tool received them, which may be absolute;
29673
+ * a path outside the workspace cannot appear in the checkpoint diff and is
29674
+ * dropped rather than mismatched.
29675
+ */
29676
+ function normalizeChangedFilePaths(changedPaths, workspaceCwd) {
29677
+ const workspacePrefix = workspaceCwd.endsWith("/") ? workspaceCwd : `${workspaceCwd}/`;
29678
+ const normalized = /* @__PURE__ */ new Set();
29679
+ for (const changedPath of changedPaths) {
29680
+ let candidate = changedPath;
29681
+ if (candidate.startsWith(workspacePrefix)) candidate = candidate.slice(workspacePrefix.length);
29682
+ else if (candidate.startsWith("/")) continue;
29683
+ else if (candidate.startsWith("./")) candidate = candidate.slice(2);
29684
+ if (candidate.length > 0) normalized.add(candidate);
29685
+ }
29686
+ return normalized;
29687
+ }
29688
+ const DIFF_FILE_HEADER = /^diff --git /gm;
29689
+ function splitUnifiedDiffBlocks(diff) {
29690
+ const starts = [];
29691
+ for (const match of diff.matchAll(DIFF_FILE_HEADER)) starts.push(match.index);
29692
+ return starts.map((start, index) => diff.slice(start, starts[index + 1]));
29693
+ }
29694
+ function blockNamesPath(block, allowedPaths) {
29695
+ try {
29696
+ return parsePatchFiles(block).some((patch) => patch.files.some((file) => allowedPaths.has(file.name) || file.prevName !== void 0 && allowedPaths.has(file.prevName)));
29697
+ } catch {
29698
+ return true;
29699
+ }
29700
+ }
29701
+ /**
29702
+ * Cut a unified diff down to the files a thread's own activities name.
29703
+ *
29704
+ * `changedPaths` is the raw activity path list; normalization against
29705
+ * `workspaceCwd` happens here. The caller decides what an empty path list
29706
+ * means - this function treats it literally and returns an empty diff.
29707
+ */
29708
+ function filterUnifiedDiffToChangedFiles(diff, changedPaths, workspaceCwd) {
29709
+ if (diff.length === 0) return diff;
29710
+ const allowedPaths = normalizeChangedFilePaths(changedPaths, workspaceCwd);
29711
+ return splitUnifiedDiffBlocks(diff).filter((block) => blockNamesPath(block, allowedPaths)).join("");
29712
+ }
29713
+ //#endregion
29714
+ //#region src/checkpointing/Errors.ts
29715
+ const CheckpointDiffOperation = Schema$1.Literals(["CheckpointDiffQuery.getTurnDiff", "CheckpointDiffQuery.getFullThreadDiff"]);
29716
+ /** The computed result does not satisfy the checkpoint RPC contract. */
29717
+ var CheckpointDiffResultInvalidError = class extends Schema$1.TaggedErrorClass()("CheckpointDiffResultInvalidError", {
29718
+ operation: CheckpointDiffOperation,
29719
+ threadId: ThreadId
29720
+ }) {
29721
+ get message() {
29722
+ const result = this.operation === "CheckpointDiffQuery.getTurnDiff" ? "turn diff" : "full thread diff";
29723
+ return `Checkpoint invariant violation in ${this.operation}: Computed ${result} result does not satisfy contract schema.`;
29724
+ }
29725
+ };
29726
+ /** Projection state no longer contains the requested checkpoint thread. */
29727
+ var CheckpointThreadNotFoundError = class extends Schema$1.TaggedErrorClass()("CheckpointThreadNotFoundError", {
29728
+ operation: CheckpointDiffOperation,
29729
+ threadId: ThreadId
29730
+ }) {
29731
+ get message() {
29732
+ return `Checkpoint invariant violation in ${this.operation}: Thread '${this.threadId}' not found.`;
29733
+ }
29734
+ };
29735
+ /** The checkpoint thread has no workspace path from which to compute a diff. */
29736
+ var CheckpointWorkspacePathMissingError = class extends Schema$1.TaggedErrorClass()("CheckpointWorkspacePathMissingError", {
29737
+ operation: CheckpointDiffOperation,
29738
+ threadId: ThreadId
29739
+ }) {
29740
+ get message() {
29741
+ const diff = this.operation === "CheckpointDiffQuery.getTurnDiff" ? "turn diff" : "full thread diff";
29742
+ return `Checkpoint invariant violation in ${this.operation}: Workspace path missing for thread '${this.threadId}' when computing ${diff}.`;
29743
+ }
29744
+ };
29745
+ /** The requested turn lies beyond the latest available checkpoint. */
29746
+ var CheckpointTurnRangeUnavailableError = class extends Schema$1.TaggedErrorClass()("CheckpointTurnRangeUnavailableError", {
29747
+ operation: CheckpointDiffOperation,
29748
+ threadId: ThreadId,
29749
+ requestedTurnCount: NonNegativeInt,
29750
+ availableTurnCount: NonNegativeInt
29751
+ }) {
29752
+ get message() {
29753
+ return `Checkpoint unavailable for thread ${this.threadId} turn ${this.requestedTurnCount}: Turn diff range exceeds current turn count: requested ${this.requestedTurnCount}, current ${this.availableTurnCount}.`;
29754
+ }
29755
+ };
29756
+ /** Expected checkpoint metadata does not contain the requested Git ref. */
29757
+ var CheckpointRefUnavailableError = class extends Schema$1.TaggedErrorClass()("CheckpointRefUnavailableError", {
29758
+ operation: CheckpointDiffOperation,
29759
+ threadId: ThreadId,
29760
+ turnCount: NonNegativeInt,
29761
+ checkpoint: Schema$1.Literals(["from", "to"])
29762
+ }) {
29763
+ get message() {
29764
+ return `Checkpoint unavailable for thread ${this.threadId} turn ${this.turnCount}: Checkpoint ref is unavailable for turn ${this.turnCount}.`;
29765
+ }
29766
+ };
29767
+ //#endregion
29768
+ //#region src/checkpointing/Utils.ts
29769
+ const CHECKPOINT_REFS_PREFIX = "refs/p4/checkpoints";
29770
+ function checkpointRefForThreadTurn(threadId, turnCount) {
29771
+ return CheckpointRef.make(`${CHECKPOINT_REFS_PREFIX}/${Encoding.encodeBase64Url(threadId)}/turn/${turnCount}`);
29772
+ }
29773
+ function resolveThreadWorkspaceCwd(input) {
29774
+ const worktreeCwd = input.thread.worktreePath ?? void 0;
29775
+ if (worktreeCwd) return worktreeCwd;
29776
+ return input.projects.find((project) => project.id === input.thread.projectId)?.workspaceRoot;
29777
+ }
29778
+ //#endregion
29779
+ //#region src/git/remoteRefs.ts
29780
+ function parseRemoteNamesInGitOrder(stdout) {
29781
+ const remoteNames = [];
29782
+ for (const line of stdout.split("\n")) {
29783
+ const remoteName = line.trim();
29784
+ if (remoteName.length > 0) remoteNames.push(remoteName);
29785
+ }
29786
+ return remoteNames;
29787
+ }
29788
+ function parseRemoteNames(stdout) {
29789
+ return parseRemoteNamesInGitOrder(stdout).toSorted((a, b) => b.length - a.length);
29790
+ }
29791
+ function parseRemoteRefWithRemoteNames(ref, remoteNames) {
29792
+ const trimmedRef = ref.trim();
29793
+ if (trimmedRef.length === 0) return null;
29794
+ for (const remoteName of remoteNames) {
29795
+ const remotePrefix = `${remoteName}/`;
29796
+ if (!trimmedRef.startsWith(remotePrefix)) continue;
29797
+ const branchName = trimmedRef.slice(remotePrefix.length).trim();
29798
+ if (branchName.length === 0) return null;
29799
+ return {
29800
+ remoteRef: trimmedRef,
29801
+ remoteName,
29802
+ branchName
29803
+ };
29804
+ }
29805
+ return null;
29806
+ }
29807
+ function extractBranchNameFromRemoteRef(ref, options) {
29808
+ const normalized = ref.trim();
29809
+ if (normalized.length === 0) return "";
29810
+ if (normalized.startsWith("refs/remotes/")) return extractBranchNameFromRemoteRef(normalized.slice(13), options);
29811
+ const parsedRemoteRef = parseRemoteRefWithRemoteNames(normalized, options?.remoteName ? [options.remoteName] : options?.remoteNames ?? []);
29812
+ if (parsedRemoteRef) return parsedRemoteRef.branchName;
29813
+ const firstSlash = normalized.indexOf("/");
29814
+ if (firstSlash === -1) return normalized;
29815
+ return normalized.slice(firstSlash + 1).trim();
29816
+ }
29817
+ //#endregion
29818
+ //#region src/vcs/GitVcsDriverCore.ts
29819
+ const DEFAULT_TIMEOUT_MS$5 = 3e4;
29820
+ const DEFAULT_MAX_OUTPUT_BYTES$1 = 1e6;
29821
+ const OUTPUT_TRUNCATED_MARKER$1 = "\n\n[truncated]";
29822
+ const PREPARED_COMMIT_PATCH_MAX_OUTPUT_BYTES = 49e3;
29823
+ const RANGE_COMMIT_SUMMARY_MAX_OUTPUT_BYTES = 19e3;
29824
+ const RANGE_DIFF_SUMMARY_MAX_OUTPUT_BYTES = 19e3;
29825
+ const RANGE_DIFF_PATCH_MAX_OUTPUT_BYTES = 59e3;
29826
+ const REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 12e4;
29827
+ const REVIEW_UNTRACKED_DIFF_MAX_OUTPUT_BYTES = 8e4;
29828
+ const WORKSPACE_FILES_MAX_OUTPUT_BYTES$1 = 12e4;
29829
+ const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15);
29830
+ const STATUS_UPSTREAM_REFRESH_TIMEOUT = Duration.seconds(5);
29831
+ const STATUS_UPSTREAM_REFRESH_FAILURE_BASE_COOLDOWN = Duration.seconds(30);
29832
+ const STATUS_UPSTREAM_REFRESH_FAILURE_MAX_COOLDOWN = Duration.minutes(15);
29833
+ const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2048;
29834
+ const REPOSITORY_PATHS_CACHE_CAPACITY = 2048;
29835
+ const REPOSITORY_PATHS_CACHE_TTL = Duration.minutes(10);
29836
+ const REPOSITORY_PATHS_REFRESH_COALESCE_TTL = Duration.seconds(5);
29837
+ const NON_REPOSITORY_PATHS_CACHE_TTL = Duration.seconds(1);
29838
+ const LIST_REFS_SNAPSHOT_CACHE_CAPACITY = 64;
29839
+ const LIST_REFS_SNAPSHOT_CACHE_TTL = Duration.minutes(2);
29840
+ const LIST_REFS_REFRESH_COALESCE_TTL = Duration.seconds(5);
29841
+ const LIST_REFS_REFRESH_FAILURE_COOLDOWN = Duration.seconds(30);
29842
+ const STATUS_UPSTREAM_REFRESH_ENV = Object.freeze({
29843
+ GCM_INTERACTIVE: "never",
29844
+ GIT_ASKPASS: "",
29845
+ GIT_TERMINAL_PROMPT: "0",
29846
+ SSH_ASKPASS: "",
29847
+ SSH_ASKPASS_REQUIRE: "never"
29848
+ });
29849
+ const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"];
29850
+ const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100;
29851
+ const NON_REPOSITORY_STATUS_DETAILS = Object.freeze({
29852
+ isRepo: false,
29853
+ hasOriginRemote: false,
29854
+ isDefaultBranch: false,
29855
+ branch: null,
29856
+ upstreamRef: null,
29857
+ hasWorkingTreeChanges: false,
29858
+ workingTree: {
29859
+ files: [],
29860
+ insertions: 0,
29861
+ deletions: 0
29862
+ },
29863
+ hasUpstream: false,
29864
+ aheadCount: 0,
29865
+ behindCount: 0,
29866
+ aheadOfDefaultCount: 0
29867
+ });
29868
+ const NON_REPOSITORY_REMOTE_STATUS_DETAILS = Object.freeze({
29869
+ isRepo: false,
29870
+ isDefaultBranch: false,
29871
+ branch: null,
29872
+ upstreamRef: null,
29873
+ hasUpstream: false,
29874
+ aheadCount: 0,
29875
+ behindCount: 0,
29876
+ aheadOfDefaultCount: 0
29877
+ });
29878
+ var StatusRemoteRefreshCacheKey = class extends Data.Class {};
29879
+ function statusUpstreamRefreshFailureCooldown(consecutiveFailures) {
29880
+ const exponent = Math.max(0, consecutiveFailures - 1);
29881
+ const cooldownMs = Duration.toMillis(STATUS_UPSTREAM_REFRESH_FAILURE_BASE_COOLDOWN) * Math.pow(2, exponent);
29882
+ return Duration.min(Duration.millis(cooldownMs), STATUS_UPSTREAM_REFRESH_FAILURE_MAX_COOLDOWN);
29883
+ }
29884
+ var GitRefsSnapshotCacheKey = class extends Data.Class {};
29885
+ var GitRefsRefreshCacheKey = class extends Data.Class {};
29886
+ function parseBranchAb(value) {
29887
+ const match = value.match(/^\+(\d+)\s+-(\d+)$/);
29888
+ if (!match) return {
29889
+ ahead: 0,
29890
+ behind: 0
29891
+ };
29892
+ return {
29893
+ ahead: Number(match[1] ?? "0"),
29894
+ behind: Number(match[2] ?? "0")
29895
+ };
29896
+ }
29897
+ function parseNumstatEntries(stdout) {
29898
+ const entries = [];
29899
+ for (const line of stdout.split(/\r?\n/g)) {
29900
+ if (line.trim().length === 0) continue;
29901
+ const [addedRaw, deletedRaw, ...pathParts] = line.split(" ");
29902
+ const rawPath = pathParts.length > 1 ? (pathParts.at(-1) ?? "").trim() : pathParts.join(" ").trim();
29903
+ if (rawPath.length === 0) continue;
29904
+ const added = Number.parseInt(addedRaw ?? "0", 10);
29905
+ const deleted = Number.parseInt(deletedRaw ?? "0", 10);
29906
+ const renameArrowIndex = rawPath.indexOf(" => ");
29907
+ const normalizedPath = renameArrowIndex >= 0 ? rawPath.slice(renameArrowIndex + 4).trim() : rawPath;
29908
+ entries.push({
29909
+ path: normalizedPath.length > 0 ? normalizedPath : rawPath,
29910
+ insertions: Number.isFinite(added) ? added : 0,
29911
+ deletions: Number.isFinite(deleted) ? deleted : 0
29912
+ });
29913
+ }
29914
+ return entries;
29915
+ }
29916
+ function parsePorcelainPath(line) {
29917
+ if (line.startsWith("? ") || line.startsWith("! ")) {
29918
+ const simple = line.slice(2).trim();
29919
+ return simple.length > 0 ? simple : null;
29920
+ }
29921
+ if (!(line.startsWith("1 ") || line.startsWith("2 ") || line.startsWith("u "))) return null;
29922
+ const tabIndex = line.indexOf(" ");
29923
+ if (tabIndex >= 0) {
29924
+ const [filePath] = line.slice(tabIndex + 1).split(" ");
29925
+ return filePath?.trim().length ? filePath.trim() : null;
29926
+ }
29927
+ const filePath = line.trim().split(/\s+/g).at(-1) ?? "";
29928
+ return filePath.length > 0 ? filePath : null;
29929
+ }
29930
+ function filterBranchesForListQuery(refs, query) {
29931
+ if (!query) return refs;
29932
+ const normalizedQuery = query.toLowerCase();
29933
+ return refs.filter((refName) => refName.name.toLowerCase().includes(normalizedQuery));
29934
+ }
29935
+ function paginateBranches(input) {
29936
+ const cursor = input.cursor ?? 0;
29937
+ const limit = input.limit ?? GIT_LIST_BRANCHES_DEFAULT_LIMIT;
29938
+ const totalCount = input.refs.length;
29939
+ const refs = input.refs.slice(cursor, cursor + limit);
29940
+ return {
29941
+ refs,
29942
+ nextCursor: cursor + refs.length < totalCount ? cursor + refs.length : null,
29943
+ totalCount
29944
+ };
29945
+ }
29946
+ function parseWorktreeBranchPaths(stdout) {
29947
+ const worktreePaths = /* @__PURE__ */ new Map();
29948
+ let currentPath = null;
29949
+ let currentBranch = null;
29950
+ let currentPrunable = false;
29951
+ const flush = () => {
29952
+ if (currentPath !== null && currentBranch !== null && !currentPrunable) worktreePaths.set(currentBranch, currentPath);
29953
+ currentPath = null;
29954
+ currentBranch = null;
29955
+ currentPrunable = false;
29956
+ };
29957
+ for (const field of stdout.split("\0")) if (field === "") flush();
29958
+ else if (field.startsWith("worktree ")) currentPath = field.slice(9);
29959
+ else if (field.startsWith("branch refs/heads/")) currentBranch = field.slice(18);
29960
+ else if (field === "prunable" || field.startsWith("prunable ")) currentPrunable = true;
29961
+ flush();
29962
+ return worktreePaths;
29963
+ }
29964
+ function splitNullSeparatedPaths$1(input, truncated) {
29965
+ const parts = input.split("\0");
29966
+ if (parts.length === 0) return [];
29967
+ if (truncated && parts[parts.length - 1]?.length) parts.pop();
29968
+ return parts.filter((value) => value.length > 0);
29969
+ }
29970
+ function splitNullSeparatedGitStdoutPaths(result) {
29971
+ return splitNullSeparatedPaths$1(result.stdout, result.stdoutTruncated);
29972
+ }
29973
+ function sanitizeRemoteName(value) {
29974
+ const sanitized = value.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
29975
+ return sanitized.length > 0 ? sanitized : "fork";
29976
+ }
29977
+ function parseRemoteFetchUrls(stdout) {
29978
+ const remotes = /* @__PURE__ */ new Map();
29979
+ for (const line of stdout.split("\n")) {
29980
+ const trimmed = line.trim();
29981
+ if (trimmed.length === 0) continue;
29982
+ const match = /^(\S+)\s+(\S+)\s+\((fetch|push)\)$/.exec(trimmed);
29983
+ if (!match) continue;
29984
+ const [, remoteName = "", remoteUrl = "", direction = ""] = match;
29985
+ if (direction !== "fetch" || remoteName.length === 0 || remoteUrl.length === 0) continue;
29986
+ remotes.set(remoteName, remoteUrl);
29987
+ }
29988
+ return remotes;
29989
+ }
29990
+ function parseUpstreamRefWithRemoteNames(upstreamRef, remoteNames) {
29991
+ const parsed = parseRemoteRefWithRemoteNames(upstreamRef, remoteNames);
29992
+ if (!parsed) return null;
29993
+ return {
29994
+ upstreamRef,
29995
+ remoteName: parsed.remoteName,
29996
+ branchName: parsed.branchName
29997
+ };
29998
+ }
29999
+ function parseUpstreamRefByFirstSeparator(upstreamRef) {
30000
+ const separatorIndex = upstreamRef.indexOf("/");
30001
+ if (separatorIndex <= 0 || separatorIndex === upstreamRef.length - 1) return null;
30002
+ const remoteName = upstreamRef.slice(0, separatorIndex).trim();
30003
+ const branchName = upstreamRef.slice(separatorIndex + 1).trim();
30004
+ if (remoteName.length === 0 || branchName.length === 0) return null;
30005
+ return {
30006
+ upstreamRef,
30007
+ remoteName,
30008
+ branchName
30009
+ };
30010
+ }
30011
+ function parseTrackingBranchByUpstreamRef(stdout, upstreamRef) {
30012
+ for (const line of stdout.split("\n")) {
30013
+ const trimmedLine = line.trim();
30014
+ if (trimmedLine.length === 0) continue;
30015
+ const [branchNameRaw, upstreamBranchRaw = ""] = trimmedLine.split(" ");
30016
+ const branchName = branchNameRaw?.trim() ?? "";
30017
+ const candidateUpstreamRef = upstreamBranchRaw.trim();
30018
+ if (branchName.length === 0 || candidateUpstreamRef.length === 0) continue;
30019
+ if (candidateUpstreamRef === upstreamRef) return branchName;
30020
+ }
30021
+ return null;
30022
+ }
30023
+ function deriveLocalBranchNameFromRemoteRef(branchName) {
30024
+ const separatorIndex = branchName.indexOf("/");
30025
+ if (separatorIndex <= 0 || separatorIndex === branchName.length - 1) return null;
30026
+ const localBranch = branchName.slice(separatorIndex + 1).trim();
30027
+ return localBranch.length > 0 ? localBranch : null;
30028
+ }
30029
+ function gitCommandContext(input) {
30030
+ return {
30031
+ operation: input.operation,
30032
+ command: "git",
30033
+ cwd: input.cwd,
30034
+ argumentCount: input.args.length
30035
+ };
30036
+ }
30037
+ function parseDefaultBranchFromRemoteHeadRef(value, remoteName) {
30038
+ const trimmed = value.trim();
30039
+ const prefix = `refs/remotes/${remoteName}/`;
30040
+ if (!trimmed.startsWith(prefix)) return null;
30041
+ const refName = trimmed.slice(prefix.length).trim();
30042
+ return refName.length > 0 ? refName : null;
30043
+ }
30044
+ function isMissingGitCwdError(error) {
30045
+ if (!(error.cause instanceof PlatformError.PlatformError)) return false;
30046
+ const reason = error.cause.reason;
30047
+ if (reason._tag === "NotFound") return reason.pathOrDescriptor === error.cwd;
30048
+ return reason._tag === "BadResource" && reason.pathOrDescriptor === error.cwd && typeof reason.cause === "object" && reason.cause !== null && "code" in reason.cause && reason.cause.code === "ENOTDIR";
30049
+ }
30050
+ function isNonRepositoryGitStderr(stderr) {
30051
+ return stderr.toLowerCase().includes("not a git repository");
30052
+ }
30053
+ const nowUnixNano = DateTime.now.pipe(Effect.map((now) => BigInt(DateTime.toEpochMillis(now)) * 1000000n));
30054
+ const addCurrentSpanEvent = (name, attributes) => Effect.gen(function* () {
30055
+ const span = yield* Effect.currentSpan;
30056
+ const timestamp = yield* nowUnixNano;
30057
+ yield* Effect.sync(() => {
30058
+ span.event(name, timestamp, compactTraceAttributes(attributes));
30059
+ });
30060
+ }).pipe(Effect.catchTags({ NoSuchElementError: () => Effect.void }));
30061
+ function trace2ChildKey(record) {
30062
+ const childId = record.child_id;
30063
+ if (typeof childId === "number" || typeof childId === "string") return String(childId);
30064
+ const hookName = record.hook_name;
30065
+ return typeof hookName === "string" && hookName.trim().length > 0 ? hookName.trim() : null;
30066
+ }
30067
+ const Trace2Record = Schema$1.Record(Schema$1.String, Schema$1.Unknown);
30068
+ const createTrace2Monitor = Effect.fn("createTrace2Monitor")(function* (input, progress) {
30069
+ if (!progress?.onHookStarted && !progress?.onHookFinished) return {
30070
+ env: {},
30071
+ flush: Effect.void
30072
+ };
30073
+ const fs = yield* FileSystem.FileSystem;
30074
+ const path = yield* Path.Path;
30075
+ const traceFilePath = yield* fs.makeTempFileScoped({
30076
+ prefix: `p4code-git-trace2-${process.pid}-`,
30077
+ suffix: ".json"
30078
+ });
30079
+ const hookStartByChildKey = /* @__PURE__ */ new Map();
30080
+ const traceTailState = yield* Ref.make({
30081
+ processedChars: 0,
30082
+ remainder: ""
30083
+ });
30084
+ const handleTraceLine = Effect.fn("handleTraceLine")(function* (line) {
30085
+ const trimmedLine = line.trim();
30086
+ if (trimmedLine.length === 0) return;
30087
+ const traceRecord = decodeJsonResult(Trace2Record)(trimmedLine);
30088
+ if (Result.isFailure(traceRecord)) {
30089
+ yield* Effect.logDebug(`GitVcsDriver.trace2: failed to parse trace line for ${input.operation} in ${input.cwd} (${input.args.length} arguments)`, traceRecord.failure);
30090
+ return;
30091
+ }
30092
+ if (traceRecord.success.child_class !== "hook") return;
30093
+ const event = traceRecord.success.event;
30094
+ const childKey = trace2ChildKey(traceRecord.success);
30095
+ if (childKey === null) return;
30096
+ const started = hookStartByChildKey.get(childKey);
30097
+ const hookNameFromEvent = typeof traceRecord.success.hook_name === "string" ? traceRecord.success.hook_name.trim() : "";
30098
+ const hookName = hookNameFromEvent.length > 0 ? hookNameFromEvent : started?.hookName ?? "";
30099
+ if (hookName.length === 0) return;
30100
+ if (event === "child_start") {
30101
+ const now = yield* DateTime.now;
30102
+ hookStartByChildKey.set(childKey, {
30103
+ hookName,
30104
+ startedAtMs: DateTime.toEpochMillis(now)
30105
+ });
30106
+ yield* addCurrentSpanEvent("git.hook.started", { hookName });
30107
+ if (progress.onHookStarted) yield* progress.onHookStarted(hookName);
30108
+ return;
30109
+ }
30110
+ if (event === "child_exit") {
30111
+ hookStartByChildKey.delete(childKey);
30112
+ const code = traceRecord.success.exitCode;
30113
+ const exitCode = typeof code === "number" && Number.isInteger(code) ? code : null;
30114
+ const now = yield* DateTime.now;
30115
+ const durationMs = started ? Math.max(0, DateTime.toEpochMillis(now) - started.startedAtMs) : null;
30116
+ yield* addCurrentSpanEvent("git.hook.finished", {
30117
+ hookName: started?.hookName ?? hookName,
30118
+ exitCode,
30119
+ durationMs
30120
+ });
29401
30121
  if (progress.onHookFinished) yield* progress.onHookFinished({
29402
30122
  hookName: started?.hookName ?? hookName,
29403
30123
  exitCode,
@@ -31596,6 +32316,32 @@ function buildTurnDiffResult(input, diff) {
31596
32316
  const make$39 = Effect.gen(function* () {
31597
32317
  const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
31598
32318
  const checkpointStore = yield* CheckpointStore;
32319
+ const threadActivities = yield* ProjectionThreadActivityRepository;
32320
+ /**
32321
+ * The file paths this thread's own tool activities name, optionally limited
32322
+ * to a set of turns. This is the attribution a checkpoint cannot provide: a
32323
+ * checkpoint snapshots the whole tree, so on a shared worktree its diff also
32324
+ * contains other threads' edits, while activities are per-thread by
32325
+ * construction.
32326
+ */
32327
+ const collectThreadChangedFilePaths = Effect.fn("CheckpointDiffQuery.collectThreadChangedFilePaths")(function* (threadId, turnIds) {
32328
+ const activities = yield* threadActivities.listByThreadId({ threadId });
32329
+ const changedPaths = /* @__PURE__ */ new Set();
32330
+ for (const activity of activities) {
32331
+ if (activity.tone !== "tool") continue;
32332
+ if (turnIds !== null && (activity.turnId === null || !turnIds.has(activity.turnId))) continue;
32333
+ for (const changedPath of collectActivityChangedFilePaths(activity.payload)) changedPaths.add(changedPath);
32334
+ }
32335
+ return changedPaths;
32336
+ });
32337
+ /**
32338
+ * A thread whose activities name no paths at all gets the unfiltered diff.
32339
+ * That is the deliberate escape valve: a thread working purely through shell
32340
+ * commands never names the files it touched, and erasing its whole diff
32341
+ * would be worse than the shared-worktree contamination this filter exists
32342
+ * to remove.
32343
+ */
32344
+ const attributeDiffToThread = (diff, changedPaths, workspaceCwd) => changedPaths.size === 0 ? diff : filterUnifiedDiffToChangedFiles(diff, changedPaths, workspaceCwd);
31599
32345
  const getTurnDiff = Effect.fn("getTurnDiff")(function* (input) {
31600
32346
  const operation = "CheckpointDiffQuery.getTurnDiff";
31601
32347
  const ignoreWhitespace = input.ignoreWhitespace ?? true;
@@ -31649,13 +32395,16 @@ const make$39 = Effect.gen(function* () {
31649
32395
  turnCount: input.toTurnCount,
31650
32396
  checkpoint: "to"
31651
32397
  });
31652
- const turnDiff = buildTurnDiffResult(input, yield* checkpointStore.diffCheckpoints({
32398
+ const diff = yield* checkpointStore.diffCheckpoints({
31653
32399
  cwd: workspaceCwd,
31654
32400
  fromCheckpointRef,
31655
32401
  toCheckpointRef,
31656
32402
  fallbackFromToHead: false,
31657
32403
  ignoreWhitespace
31658
- }).pipe(Effect.withSpan("checkpoint.turnDiff.diffCheckpoints")));
32404
+ }).pipe(Effect.withSpan("checkpoint.turnDiff.diffCheckpoints"));
32405
+ const turnIdsInRange = new Set(threadContext.value.checkpoints.filter((checkpoint) => checkpoint.checkpointTurnCount > input.fromTurnCount && checkpoint.checkpointTurnCount <= input.toTurnCount).map((checkpoint) => checkpoint.turnId));
32406
+ const changedPaths = yield* collectThreadChangedFilePaths(input.threadId, turnIdsInRange);
32407
+ const turnDiff = buildTurnDiffResult(input, attributeDiffToThread(diff, changedPaths, workspaceCwd));
31659
32408
  if (!isTurnDiffResult(turnDiff)) return yield* new CheckpointDiffResultInvalidError({
31660
32409
  operation,
31661
32410
  threadId: input.threadId
@@ -31713,11 +32462,12 @@ const make$39 = Effect.gen(function* () {
31713
32462
  fallbackFromToHead: false,
31714
32463
  ignoreWhitespace
31715
32464
  }).pipe(Effect.withSpan("checkpoint.fullThread.diffCheckpoints"));
32465
+ const changedPaths = yield* collectThreadChangedFilePaths(input.threadId, null);
31716
32466
  const turnDiff = buildTurnDiffResult({
31717
32467
  threadId: input.threadId,
31718
32468
  fromTurnCount: 0,
31719
32469
  toTurnCount: input.toTurnCount
31720
- }, diff);
32470
+ }, attributeDiffToThread(diff, changedPaths, workspaceCwd));
31721
32471
  if (!isTurnDiffResult(turnDiff)) return yield* new CheckpointDiffResultInvalidError({
31722
32472
  operation,
31723
32473
  threadId: input.threadId
@@ -32297,7 +33047,7 @@ const normalizeDispatchCommand = (command) => Effect.gen(function* () {
32297
33047
  const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
32298
33048
  taskId,
32299
33049
  projectId: input.projectId ?? null,
32300
- repositoryKey,
33050
+ repositoryKey: repositoryKey ?? input.repositoryKey ?? null,
32301
33051
  threadId: input.threadId ?? null,
32302
33052
  title: input.title,
32303
33053
  body: input.body ?? "",
@@ -35368,6 +36118,18 @@ const requireTaskCapability = Effect.fn("mcp.requireTaskCapability")(function* (
35368
36118
  });
35369
36119
  return invocation;
35370
36120
  });
36121
+ /** Third of the trio, for the same reason the second exists. */
36122
+ const requireThreadCapability = Effect.fn("mcp.requireThreadCapability")(function* () {
36123
+ const invocation = yield* McpInvocationContext;
36124
+ if (!invocation.capabilities.has("threads")) return yield* new ThreadToolUnavailableError({
36125
+ capability: "threads",
36126
+ environmentId: invocation.environmentId,
36127
+ threadId: invocation.threadId,
36128
+ providerSessionId: invocation.providerSessionId,
36129
+ providerInstanceId: invocation.providerInstanceId
36130
+ });
36131
+ return invocation;
36132
+ });
35371
36133
  //#endregion
35372
36134
  //#region src/mcp/PreviewAutomationBroker.ts
35373
36135
  var PreviewAutomationBroker = class extends Context.Service()("@p4code/cli/mcp/PreviewAutomationBroker") {};
@@ -44316,7 +45078,11 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
44316
45078
  threadId: ThreadId.make(request.threadId),
44317
45079
  providerSessionId,
44318
45080
  providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),
44319
- capabilities: /* @__PURE__ */ new Set(["preview", "tasks"]),
45081
+ capabilities: /* @__PURE__ */ new Set([
45082
+ "preview",
45083
+ "tasks",
45084
+ "threads"
45085
+ ]),
44320
45086
  issuedAt
44321
45087
  };
44322
45088
  yield* SynchronizedRef.update(state, ({ records }) => {
@@ -81339,7 +82105,7 @@ const invokeTargeted = (operation, input, timeoutMs) => {
81339
82105
  const { tabId, ...operationInput } = input;
81340
82106
  return invoke(operation, operationInput, timeoutMs, tabId);
81341
82107
  };
81342
- const handlers$1 = {
82108
+ const handlers$2 = {
81343
82109
  preview_status: (input) => invokeTargeted("status", input ?? {}),
81344
82110
  preview_open: (input) => invokeTargeted("open", normalizePreviewOpenInput(input)),
81345
82111
  preview_navigate: (input) => invokeTargeted("navigate", input, input.timeoutMs),
@@ -81355,10 +82121,10 @@ const handlers$1 = {
81355
82121
  preview_recording_start: (input) => invokeTargeted("recordingStart", input ?? {}),
81356
82122
  preview_recording_stop: (input) => invokeTargeted("recordingStop", input ?? {})
81357
82123
  };
81358
- const { preview_snapshot, ...standardHandlers } = handlers$1;
82124
+ const { preview_snapshot, ...standardHandlers } = handlers$2;
81359
82125
  const PreviewStandardToolkitHandlersLive = PreviewStandardToolkit.toLayer(standardHandlers);
81360
82126
  const PreviewSnapshotToolkitHandlersLive = PreviewSnapshotToolkit.toLayer({ preview_snapshot });
81361
- PreviewToolkit.toLayer(handlers$1);
82127
+ PreviewToolkit.toLayer(handlers$2);
81362
82128
  //#endregion
81363
82129
  //#region src/mcp/toolkits/tasks/tools.ts
81364
82130
  const dependencies = [McpInvocationContext, TaskRepository];
@@ -81504,6 +82270,93 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
81504
82270
  })
81505
82271
  });
81506
82272
  //#endregion
82273
+ //#region src/mcp/toolkits/threads/tools.ts
82274
+ /**
82275
+ * Self-scoped on purpose: neither tool takes a thread id. The thread acted on
82276
+ * is always the one in `McpInvocationScope`, exactly as `task_current` resolves
82277
+ * it, so a leaked per-session token cannot reach any other thread. Delete,
82278
+ * create and archive are deliberately not exposed; settle could never succeed
82279
+ * from here because the decider refuses it while the calling session is alive.
82280
+ */
82281
+ const ThreadRenameTool = Tool.make("thread_rename", {
82282
+ description: "Rename this agent session's own thread. Use it when the seeded title no longer matches what the thread turned out to be about. Only the current thread can be renamed; no other thread is reachable.",
82283
+ parameters: ThreadRenameInput,
82284
+ success: ThreadRenameResult,
82285
+ failure: ThreadControlToolError,
82286
+ dependencies: [
82287
+ McpInvocationContext,
82288
+ OrchestrationEngineService,
82289
+ Crypto.Crypto
82290
+ ]
82291
+ }).annotate(Tool.Title, "Rename this thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
82292
+ const MemoryAppendTool = Tool.make("memory_append", {
82293
+ description: "Append a block of markdown to the user-scope memory file (CLAUDE.md by default, AGENTS.md on request). Use it to record a durable preference or fact the user asked to remember. Appends only - it never rewrites or removes existing memory.",
82294
+ parameters: MemoryAppendInput,
82295
+ success: MemoryAppendResult,
82296
+ failure: ThreadControlToolError,
82297
+ dependencies: [
82298
+ McpInvocationContext,
82299
+ ServerSettingsService,
82300
+ FileSystem.FileSystem,
82301
+ Path.Path
82302
+ ]
82303
+ }).annotate(Tool.Title, "Append to memory").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
82304
+ const ThreadToolkit = Toolkit.make(ThreadRenameTool, MemoryAppendTool);
82305
+ //#endregion
82306
+ //#region src/mcp/toolkits/threads/handlers.ts
82307
+ const DEFAULT_MEMORY_APPEND_TARGET = "CLAUDE.md";
82308
+ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
82309
+ thread_rename: (input) => Effect.gen(function* () {
82310
+ const invocation = yield* requireThreadCapability();
82311
+ const engine = yield* OrchestrationEngineService;
82312
+ const crypto = yield* Crypto.Crypto;
82313
+ const commandId = CommandId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
82314
+ yield* engine.dispatch({
82315
+ type: "thread.meta.update",
82316
+ commandId,
82317
+ threadId: invocation.threadId,
82318
+ title: input.title
82319
+ }).pipe(Effect.mapError((cause) => new ThreadRenameRejectedError({
82320
+ threadId: invocation.threadId,
82321
+ detail: cause.message
82322
+ })));
82323
+ return {
82324
+ threadId: invocation.threadId,
82325
+ title: input.title
82326
+ };
82327
+ }),
82328
+ memory_append: (input) => Effect.gen(function* () {
82329
+ yield* requireThreadCapability();
82330
+ const file = input.file ?? DEFAULT_MEMORY_APPEND_TARGET;
82331
+ if (!isMemoryFileName(file)) return yield* new MemoryAppendFailedError({
82332
+ file,
82333
+ detail: `${file} is not an allowlisted memory file.`
82334
+ });
82335
+ const memoryRoot = yield* resolveClaudeConfigDirPath({ homePath: (yield* (yield* ServerSettingsService).getSettings.pipe(Effect.orElseSucceed(() => void 0)))?.providers.claudeAgent.homePath ?? "" }, process.env);
82336
+ const path = yield* Path.Path;
82337
+ const fileSystem = yield* FileSystem.FileSystem;
82338
+ const filePath = path.join(memoryRoot, file);
82339
+ const existing = yield* fileSystem.readFileString(filePath).pipe(Effect.orElseSucceed(() => ""));
82340
+ const block = input.text.trim();
82341
+ const contents = existing.trim().length === 0 ? `${block}\n` : `${existing.trimEnd()}\n\n${block}\n`;
82342
+ if (contents.length > 524288) return yield* new MemoryAppendFailedError({
82343
+ file,
82344
+ detail: `Appending would grow ${file} past ${AGENT_ASSET_FILE_MAX_CHARS} characters.`
82345
+ });
82346
+ yield* writeFileStringAtomically({
82347
+ filePath,
82348
+ contents
82349
+ }).pipe(Effect.mapError((cause) => new MemoryAppendFailedError({
82350
+ file,
82351
+ detail: cause.message
82352
+ })));
82353
+ return {
82354
+ file,
82355
+ path: filePath
82356
+ };
82357
+ })
82358
+ });
82359
+ //#endregion
81507
82360
  //#region src/mcp/McpHttpServer.ts
81508
82361
  const unauthorized = HttpServerResponse.jsonUnsafe({
81509
82362
  error: "invalid_mcp_credential",
@@ -81607,12 +82460,13 @@ const PreviewStandardToolkitRegistrationLive = McpServer.toolkit(PreviewStandard
81607
82460
  const PreviewSnapshotRegistrationLive = Layer.effectDiscard(registerPreviewSnapshot()).pipe(Layer.provide(PreviewSnapshotToolkitHandlersLive));
81608
82461
  const PreviewToolkitRegistrationLive = Layer.mergeAll(PreviewStandardToolkitRegistrationLive, PreviewSnapshotRegistrationLive);
81609
82462
  const TaskToolkitRegistrationLive = McpServer.toolkit(TaskToolkit).pipe(Layer.provide(TaskToolkitHandlersLive));
82463
+ const ThreadToolkitRegistrationLive = McpServer.toolkit(ThreadToolkit).pipe(Layer.provide(ThreadToolkitHandlersLive));
81610
82464
  const McpTransportLive = McpServer.layerHttp({
81611
82465
  name: "P4Code",
81612
82466
  version,
81613
82467
  path: "/mcp"
81614
82468
  }).pipe(Layer.provide(McpAuthMiddlewareLive));
81615
- const layer = Layer.mergeAll(PreviewToolkitRegistrationLive, TaskToolkitRegistrationLive).pipe(Layer.provideMerge(McpTransportLive));
82469
+ const layer = Layer.mergeAll(PreviewToolkitRegistrationLive, TaskToolkitRegistrationLive, ThreadToolkitRegistrationLive).pipe(Layer.provideMerge(McpTransportLive));
81616
82470
  //#endregion
81617
82471
  //#region src/orchestration/Services/CheckpointReactor.ts
81618
82472
  /**
@@ -82946,992 +83800,460 @@ const make$2 = Effect.gen(function* () {
82946
83800
  const requestedModelSelection = options?.modelSelection;
82947
83801
  const resolveActiveSession = (threadId) => providerService.listSessions().pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === threadId)));
82948
83802
  const activeSession = yield* resolveActiveSession(threadId);
82949
- const activeThreadSession = thread.session !== null && thread.session.status !== "stopped" && activeSession ? thread.session : null;
82950
- if (activeThreadSession !== null && activeSession !== void 0 && (activeThreadSession.providerInstanceId === void 0 || activeSession.providerInstanceId === void 0)) return yield* new ProviderAdapterRequestError({
82951
- provider: providerErrorLabel(activeThreadSession.providerName ?? void 0),
82952
- method: "thread.turn.start",
82953
- detail: `Thread '${threadId}' has an active provider session without a provider instance id.`
82954
- });
82955
- const currentInstanceId = activeThreadSession !== null && activeSession !== void 0 && activeSession.providerInstanceId !== void 0 ? activeSession.providerInstanceId : thread.modelSelection.instanceId;
82956
- const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;
82957
- const desiredInstanceId = desiredModelSelection.instanceId;
82958
- const currentInfo = yield* providerService.getInstanceInfo(currentInstanceId).pipe(Effect.mapError(() => new ProviderAdapterRequestError({
82959
- provider: providerErrorLabelFromInstanceHint({
82960
- instanceId: String(currentInstanceId),
82961
- modelSelectionInstanceId: String(thread.modelSelection.instanceId),
82962
- sessionProvider: thread.session?.providerName ?? void 0
82963
- }),
82964
- method: "thread.turn.start",
82965
- detail: `Thread '${threadId}' references unknown provider instance '${currentInstanceId}'. The instance is not configured in this build.`
82966
- })));
82967
- const desiredInfo = yield* providerService.getInstanceInfo(desiredInstanceId).pipe(Effect.mapError(() => new ProviderAdapterRequestError({
82968
- provider: providerErrorLabelFromInstanceHint({ instanceId: String(desiredModelSelection.instanceId) }),
82969
- method: "thread.turn.start",
82970
- detail: `Requested provider instance '${desiredInstanceId}' is not configured in this build.`
82971
- })));
82972
- const desiredDriverKind = desiredInfo.driverKind;
82973
- if (!isProviderDriverKind(desiredDriverKind)) return yield* new ProviderAdapterRequestError({
82974
- provider: providerErrorLabel(String(desiredDriverKind)),
82975
- method: "thread.turn.start",
82976
- detail: `Requested provider instance '${desiredInstanceId}' uses unknown provider driver '${desiredDriverKind}'. The driver is not installed in this build.`
82977
- });
82978
- const preferredProvider = desiredDriverKind;
82979
- if (options?.pendingTurnStart === true && thread.session?.status !== "running") yield* setThreadSession({
82980
- threadId,
82981
- session: {
82982
- threadId,
82983
- status: "starting",
82984
- providerName: activeSession?.provider ?? preferredProvider,
82985
- providerInstanceId: activeSession?.providerInstanceId ?? desiredInstanceId,
82986
- runtimeMode: desiredRuntimeMode,
82987
- activeTurnId: null,
82988
- lastError: null,
82989
- updatedAt: createdAt
82990
- },
82991
- createdAt
82992
- });
82993
- if (thread.session !== null) yield* rejectStartedThreadModelChangeIfRequired({
82994
- threadId,
82995
- currentModelSelection: activeSession?.model !== void 0 ? {
82996
- ...thread.modelSelection,
82997
- instanceId: currentInstanceId,
82998
- model: activeSession.model
82999
- } : thread.modelSelection,
83000
- requestedModelSelection
83001
- });
83002
- if (thread.session !== null && requestedModelSelection !== void 0 && requestedModelSelection.instanceId !== currentInstanceId) {
83003
- if (currentInfo.driverKind !== desiredInfo.driverKind) return yield* new ProviderAdapterRequestError({
83004
- provider: preferredProvider,
83005
- method: "thread.turn.start",
83006
- detail: `Thread '${threadId}' is bound to driver '${currentInfo.driverKind}' and cannot switch to '${desiredInfo.driverKind}'.`
83007
- });
83008
- if (currentInfo.continuationIdentity.continuationKey !== desiredInfo.continuationIdentity.continuationKey) return yield* new ProviderAdapterRequestError({
83009
- provider: preferredProvider,
83010
- method: "thread.turn.start",
83011
- detail: `Thread '${threadId}' cannot switch from instance '${currentInstanceId}' to '${desiredInstanceId}' because their provider resume state is incompatible.`
83012
- });
83013
- }
83014
- const project = yield* resolveProject(thread.projectId);
83015
- const effectiveCwd = resolveThreadWorkspaceCwd({
83016
- thread,
83017
- projects: project ? [project] : []
83018
- });
83019
- const startProviderSession = (input) => providerService.startSession(threadId, {
83020
- threadId,
83021
- ...preferredProvider ? { provider: preferredProvider } : {},
83022
- providerInstanceId: desiredInstanceId,
83023
- ...effectiveCwd ? { cwd: effectiveCwd } : {},
83024
- modelSelection: desiredModelSelection,
83025
- ...input?.resumeCursor !== void 0 ? { resumeCursor: input.resumeCursor } : {},
83026
- runtimeMode: desiredRuntimeMode
83027
- });
83028
- const bindSessionToThread = (session) => Effect.gen(function* () {
83029
- if (session.providerInstanceId === void 0) return yield* new ProviderAdapterRequestError({
83030
- provider: providerErrorLabel(session.provider),
83031
- method: "thread.turn.start",
83032
- detail: `Provider session '${session.threadId}' started without a provider instance id.`
83033
- });
83034
- yield* setThreadSession({
83035
- threadId,
83036
- session: {
83037
- threadId,
83038
- status: options?.pendingTurnStart === true && session.status === "ready" ? "starting" : mapProviderSessionStatusToOrchestrationStatus(session.status),
83039
- providerName: session.provider,
83040
- providerInstanceId: session.providerInstanceId,
83041
- runtimeMode: desiredRuntimeMode,
83042
- activeTurnId: null,
83043
- lastError: session.lastError ?? null,
83044
- updatedAt: session.updatedAt
83045
- },
83046
- createdAt
83047
- });
83048
- });
83049
- const existingSessionThreadId = thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null;
83050
- if (existingSessionThreadId) {
83051
- const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode;
83052
- const cwdChanged = effectiveCwd !== activeSession?.cwd;
83053
- const sessionModelSwitch = (yield* providerService.getCapabilities(desiredInstanceId)).sessionModelSwitch;
83054
- const modelChanged = requestedModelSelection !== void 0 && requestedModelSelection.model !== activeSession?.model;
83055
- const instanceChanged = requestedModelSelection !== void 0 && activeSession?.providerInstanceId !== requestedModelSelection.instanceId;
83056
- const shouldRestartForModelChange = modelChanged && sessionModelSwitch === "unsupported";
83057
- const previousModelSelection = threadModelSelections.get(threadId);
83058
- const shouldRestartForModelSelectionChange = preferredProvider === "claudeAgent" && requestedModelSelection !== void 0 && !Equal.equals(previousModelSelection, requestedModelSelection);
83059
- if (!runtimeModeChanged && !cwdChanged && !instanceChanged && !shouldRestartForModelChange && !shouldRestartForModelSelectionChange) return existingSessionThreadId;
83060
- const resumeCursor = shouldRestartForModelChange ? void 0 : activeSession?.resumeCursor ?? void 0;
83061
- yield* Effect.logInfo("provider command reactor restarting provider session", {
83062
- threadId,
83063
- existingSessionThreadId,
83064
- currentProvider: activeSession?.provider,
83065
- currentInstanceId,
83066
- desiredInstanceId,
83067
- desiredProvider: desiredModelSelection.instanceId,
83068
- currentRuntimeMode: thread.session?.runtimeMode,
83069
- desiredRuntimeMode: thread.runtimeMode,
83070
- runtimeModeChanged,
83071
- previousCwd: activeSession?.cwd,
83072
- desiredCwd: effectiveCwd,
83073
- cwdChanged,
83074
- modelChanged,
83075
- instanceChanged,
83076
- shouldRestartForModelChange,
83077
- shouldRestartForModelSelectionChange,
83078
- hasResumeCursor: resumeCursor !== void 0
83079
- });
83080
- const restartedSession = yield* startProviderSession(resumeCursor !== void 0 ? { resumeCursor } : void 0);
83081
- yield* Effect.logInfo("provider command reactor restarted provider session", {
83082
- threadId,
83083
- previousSessionId: existingSessionThreadId,
83084
- restartedSessionThreadId: restartedSession.threadId,
83085
- provider: restartedSession.provider,
83086
- runtimeMode: restartedSession.runtimeMode,
83087
- cwd: restartedSession.cwd
83088
- });
83089
- yield* bindSessionToThread(restartedSession);
83090
- return restartedSession.threadId;
83091
- }
83092
- const startedSession = yield* startProviderSession(void 0);
83093
- yield* bindSessionToThread(startedSession);
83094
- return startedSession.threadId;
83095
- });
83096
- const buildSendTurnRequestForThread = Effect.fnUntraced(function* (input) {
83097
- const thread = yield* resolveThread(input.threadId);
83098
- if (!thread) return yield* Effect.die(/* @__PURE__ */ new Error(`Thread '${input.threadId}' was not found in read model.`));
83099
- yield* ensureSessionForThread(input.threadId, input.createdAt, {
83100
- ...input.modelSelection !== void 0 ? { modelSelection: input.modelSelection } : {},
83101
- pendingTurnStart: true
83102
- });
83103
- if (input.modelSelection !== void 0) threadModelSelections.set(input.threadId, input.modelSelection);
83104
- const normalizedInput = toNonEmptyProviderInput(input.messageText);
83105
- const normalizedAttachments = input.attachments ?? [];
83106
- const activeSession = yield* providerService.listSessions().pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === input.threadId)));
83107
- const sessionModelSwitch = activeSession === void 0 ? "in-session" : activeSession.providerInstanceId === void 0 ? yield* new ProviderAdapterRequestError({
83108
- provider: providerErrorLabel(activeSession.provider),
83109
- method: "thread.turn.start",
83110
- detail: `Active provider session '${activeSession.threadId}' is missing a provider instance id.`
83111
- }) : (yield* providerService.getCapabilities(activeSession.providerInstanceId)).sessionModelSwitch;
83112
- const requestedModelSelection = input.modelSelection ?? threadModelSelections.get(input.threadId) ?? thread.modelSelection;
83113
- const modelForTurn = sessionModelSwitch === "unsupported" && input.modelSelection === void 0 ? activeSession?.model !== void 0 ? {
83114
- ...requestedModelSelection,
83115
- model: activeSession.model
83116
- } : requestedModelSelection : input.modelSelection;
83117
- return {
83118
- threadId: input.threadId,
83119
- ...normalizedInput ? { input: normalizedInput } : {},
83120
- ...normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {},
83121
- ...modelForTurn !== void 0 ? { modelSelection: modelForTurn } : {},
83122
- ...input.interactionMode !== void 0 ? { interactionMode: input.interactionMode } : {}
83123
- };
83124
- });
83125
- const maybeGenerateAndRenameWorktreeBranchForFirstTurn = Effect.fn("maybeGenerateAndRenameWorktreeBranchForFirstTurn")(function* (input) {
83126
- if (!input.branch || !input.worktreePath) return;
83127
- if (!isTemporaryWorktreeBranch(input.branch)) return;
83128
- const oldBranch = input.branch;
83129
- const cwd = input.worktreePath;
83130
- const attachments = input.attachments ?? [];
83131
- yield* Effect.gen(function* () {
83132
- const settings = yield* serverSettingsService.getSettings;
83133
- const modelSelection = settings.sourceControlWriterModelSelection === null ? settings.textGenerationModelSelection : resolveSourceControlWriterModelSelection(settings, yield* providerRegistry.getProviders);
83134
- const generated = yield* textGeneration.generateBranchName({
83135
- cwd,
83136
- message: input.messageText,
83137
- ...attachments.length > 0 ? { attachments } : {},
83138
- modelSelection
83139
- });
83140
- if (!generated) return;
83141
- const targetBranch = buildGeneratedWorktreeBranchName(generated.branch);
83142
- if (targetBranch === oldBranch) return;
83143
- const renamed = yield* gitWorkflow.renameBranch({
83144
- cwd,
83145
- oldBranch,
83146
- newBranch: targetBranch
83147
- });
83148
- yield* orchestrationEngine.dispatch({
83149
- type: "thread.meta.update",
83150
- commandId: yield* serverCommandId("worktree-branch-rename"),
83151
- threadId: input.threadId,
83152
- branch: renamed.branch,
83153
- worktreePath: cwd
83154
- });
83155
- yield* vcsStatusBroadcaster.refreshStatus(cwd).pipe(Effect.ignoreCause({ log: true }));
83156
- }).pipe(Effect.catchCause((cause) => Effect.logWarning("provider command reactor failed to generate or rename worktree branch", {
83157
- threadId: input.threadId,
83158
- cwd,
83159
- oldBranch,
83160
- cause: Cause.pretty(cause)
83161
- })));
83162
- });
83163
- const maybeGenerateThreadTitleForFirstTurn = Effect.fn("maybeGenerateThreadTitleForFirstTurn")(function* (input) {
83164
- const attachments = input.attachments ?? [];
83165
- yield* Effect.gen(function* () {
83166
- const { textGenerationModelSelection: modelSelection } = yield* serverSettingsService.getSettings;
83167
- const generated = yield* textGeneration.generateThreadTitle({
83168
- cwd: input.cwd,
83169
- message: input.messageText,
83170
- ...attachments.length > 0 ? { attachments } : {},
83171
- modelSelection
83172
- });
83173
- if (!generated) return;
83174
- const thread = yield* resolveThread(input.threadId);
83175
- if (!thread) return;
83176
- if (!canReplaceThreadTitle(thread.title, input.titleSeed)) return;
83177
- yield* orchestrationEngine.dispatch({
83178
- type: "thread.meta.update",
83179
- commandId: yield* serverCommandId("thread-title-rename"),
83180
- threadId: input.threadId,
83181
- title: generated.title
83182
- });
83183
- }).pipe(Effect.catchCause((cause) => Effect.logWarning("provider command reactor failed to generate or rename thread title", {
83184
- threadId: input.threadId,
83185
- cwd: input.cwd,
83186
- cause: Cause.pretty(cause)
83187
- })));
83188
- });
83189
- const processTurnStartRequested = Effect.fn("processTurnStartRequested")(function* (event) {
83190
- const key = turnStartKeyForEvent(event);
83191
- if (yield* hasHandledTurnStartRecently(key)) return;
83192
- const thread = yield* resolveThread(event.payload.threadId);
83193
- if (!thread) return;
83194
- const message = thread.messages.find((entry) => entry.id === event.payload.messageId);
83195
- if (!message || message.role !== "user") {
83196
- yield* appendProviderFailureActivity({
83197
- threadId: event.payload.threadId,
83198
- kind: "provider.turn.start.failed",
83199
- summary: "Provider turn start failed",
83200
- detail: `User message '${event.payload.messageId}' was not found for turn start request.`,
83201
- turnId: null,
83202
- createdAt: event.payload.createdAt
83203
- });
83204
- return;
83205
- }
83206
- if (thread.messages.filter((entry) => entry.role === "user").length === 1) {
83207
- const project = yield* resolveProject(thread.projectId);
83208
- const generationCwd = resolveThreadWorkspaceCwd({
83209
- thread,
83210
- projects: project ? [project] : []
83211
- }) ?? process.cwd();
83212
- const generationInput = {
83213
- messageText: message.text,
83214
- ...message.attachments !== void 0 ? { attachments: message.attachments } : {},
83215
- ...event.payload.titleSeed !== void 0 ? { titleSeed: event.payload.titleSeed } : {}
83216
- };
83217
- yield* maybeGenerateAndRenameWorktreeBranchForFirstTurn({
83218
- threadId: event.payload.threadId,
83219
- branch: thread.branch,
83220
- worktreePath: thread.worktreePath,
83221
- ...generationInput
83222
- }).pipe(Effect.forkScoped);
83223
- if (canReplaceThreadTitle(thread.title, event.payload.titleSeed)) yield* maybeGenerateThreadTitleForFirstTurn({
83224
- threadId: event.payload.threadId,
83225
- cwd: generationCwd,
83226
- ...generationInput
83227
- }).pipe(Effect.forkScoped);
83228
- }
83229
- const handleTurnStartFailure = (cause) => {
83230
- if (Cause.hasInterruptsOnly(cause)) return Effect.void;
83231
- const detail = formatFailureDetail(cause);
83232
- return setThreadSessionErrorOnTurnStartFailure({
83233
- threadId: event.payload.threadId,
83234
- detail,
83235
- createdAt: event.payload.createdAt
83236
- }).pipe(Effect.flatMap(() => appendProviderFailureActivity({
83237
- threadId: event.payload.threadId,
83238
- kind: "provider.turn.start.failed",
83239
- summary: "Provider turn start failed",
83240
- detail,
83241
- turnId: null,
83242
- createdAt: event.payload.createdAt
83243
- })), Effect.asVoid);
83244
- };
83245
- const recoverTurnStartFailure = (cause) => handleTurnStartFailure(cause).pipe(Effect.catchCause((recoveryCause) => Effect.logWarning("provider command reactor failed to recover turn start failure", {
83246
- eventType: event.type,
83247
- threadId: event.payload.threadId,
83248
- cause: Cause.pretty(recoveryCause),
83249
- originalCause: Cause.pretty(cause)
83250
- })));
83251
- const sendTurnRequest = yield* buildSendTurnRequestForThread({
83252
- threadId: event.payload.threadId,
83253
- messageText: message.text,
83254
- ...message.attachments !== void 0 ? { attachments: message.attachments } : {},
83255
- ...event.payload.modelSelection !== void 0 ? { modelSelection: event.payload.modelSelection } : {},
83256
- interactionMode: event.payload.interactionMode,
83257
- createdAt: event.payload.createdAt
83258
- }).pipe(Effect.map(Option.some), Effect.catchCause((cause) => handleTurnStartFailure(cause).pipe(Effect.as(Option.none()))));
83259
- if (Option.isNone(sendTurnRequest)) return;
83260
- yield* providerService.sendTurn(sendTurnRequest.value).pipe(Effect.catchCause(recoverTurnStartFailure), Effect.forkScoped);
83261
- });
83262
- const processTurnInterruptRequested = Effect.fn("processTurnInterruptRequested")(function* (event) {
83263
- const thread = yield* resolveThread(event.payload.threadId);
83264
- if (!thread) return;
83265
- if (!(thread.session && thread.session.status !== "stopped")) return yield* appendProviderFailureActivity({
83266
- threadId: event.payload.threadId,
83267
- kind: "provider.turn.interrupt.failed",
83268
- summary: "Provider turn interrupt failed",
83269
- detail: "No active provider session is bound to this thread.",
83270
- turnId: event.payload.turnId ?? null,
83271
- createdAt: event.payload.createdAt
83272
- });
83273
- yield* providerService.interruptTurn({ threadId: event.payload.threadId });
83274
- yield* appendTurnInterruptedActivity({
83275
- threadId: event.payload.threadId,
83276
- turnId: event.payload.turnId ?? null,
83277
- createdAt: event.payload.createdAt
83278
- });
83279
- });
83280
- const processApprovalResponseRequested = Effect.fn("processApprovalResponseRequested")(function* (event) {
83281
- const thread = yield* resolveThread(event.payload.threadId);
83282
- if (!thread) return;
83283
- if (!(thread.session && thread.session.status !== "stopped")) return yield* appendProviderFailureActivity({
83284
- threadId: event.payload.threadId,
83285
- kind: "provider.approval.respond.failed",
83286
- summary: "Provider approval response failed",
83287
- detail: "No active provider session is bound to this thread.",
83288
- turnId: null,
83289
- createdAt: event.payload.createdAt,
83290
- requestId: event.payload.requestId
83291
- });
83292
- yield* providerService.respondToRequest({
83293
- threadId: event.payload.threadId,
83294
- requestId: event.payload.requestId,
83295
- decision: event.payload.decision
83296
- }).pipe(Effect.catchCause((cause) => appendProviderFailureActivity({
83297
- threadId: event.payload.threadId,
83298
- kind: "provider.approval.respond.failed",
83299
- summary: "Provider approval response failed",
83300
- detail: isUnknownPendingApprovalRequestError(cause) ? stalePendingRequestDetail("approval", event.payload.requestId) : Cause.pretty(cause),
83301
- turnId: null,
83302
- createdAt: event.payload.createdAt,
83303
- requestId: event.payload.requestId
83304
- })));
83305
- });
83306
- const processUserInputResponseRequested = Effect.fn("processUserInputResponseRequested")(function* (event) {
83307
- const thread = yield* resolveThread(event.payload.threadId);
83308
- if (!thread) return;
83309
- if (!(thread.session && thread.session.status !== "stopped")) return yield* appendProviderFailureActivity({
83310
- threadId: event.payload.threadId,
83311
- kind: "provider.user-input.respond.failed",
83312
- summary: "Provider user input response failed",
83313
- detail: "No active provider session is bound to this thread.",
83314
- turnId: null,
83315
- createdAt: event.payload.createdAt,
83316
- requestId: event.payload.requestId
83317
- });
83318
- yield* providerService.respondToUserInput({
83319
- threadId: event.payload.threadId,
83320
- requestId: event.payload.requestId,
83321
- answers: event.payload.answers
83322
- }).pipe(Effect.catchCause((cause) => appendProviderFailureActivity({
83323
- threadId: event.payload.threadId,
83324
- kind: "provider.user-input.respond.failed",
83325
- summary: "Provider user input response failed",
83326
- detail: isUnknownPendingUserInputRequestError(cause) ? stalePendingRequestDetail("user-input", event.payload.requestId) : Cause.pretty(cause),
83327
- turnId: null,
83328
- createdAt: event.payload.createdAt,
83329
- requestId: event.payload.requestId
83803
+ const activeThreadSession = thread.session !== null && thread.session.status !== "stopped" && activeSession ? thread.session : null;
83804
+ if (activeThreadSession !== null && activeSession !== void 0 && (activeThreadSession.providerInstanceId === void 0 || activeSession.providerInstanceId === void 0)) return yield* new ProviderAdapterRequestError({
83805
+ provider: providerErrorLabel(activeThreadSession.providerName ?? void 0),
83806
+ method: "thread.turn.start",
83807
+ detail: `Thread '${threadId}' has an active provider session without a provider instance id.`
83808
+ });
83809
+ const currentInstanceId = activeThreadSession !== null && activeSession !== void 0 && activeSession.providerInstanceId !== void 0 ? activeSession.providerInstanceId : thread.modelSelection.instanceId;
83810
+ const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;
83811
+ const desiredInstanceId = desiredModelSelection.instanceId;
83812
+ const currentInfo = yield* providerService.getInstanceInfo(currentInstanceId).pipe(Effect.mapError(() => new ProviderAdapterRequestError({
83813
+ provider: providerErrorLabelFromInstanceHint({
83814
+ instanceId: String(currentInstanceId),
83815
+ modelSelectionInstanceId: String(thread.modelSelection.instanceId),
83816
+ sessionProvider: thread.session?.providerName ?? void 0
83817
+ }),
83818
+ method: "thread.turn.start",
83819
+ detail: `Thread '${threadId}' references unknown provider instance '${currentInstanceId}'. The instance is not configured in this build.`
83330
83820
  })));
83331
- });
83332
- const processSessionStopRequested = Effect.fn("processSessionStopRequested")(function* (event) {
83333
- const thread = yield* resolveThread(event.payload.threadId);
83334
- if (!thread) return;
83335
- const now = event.payload.createdAt;
83336
- if (thread.session && thread.session.status !== "stopped") yield* providerService.stopSession({ threadId: thread.id });
83337
- yield* setThreadSession({
83338
- threadId: thread.id,
83821
+ const desiredInfo = yield* providerService.getInstanceInfo(desiredInstanceId).pipe(Effect.mapError(() => new ProviderAdapterRequestError({
83822
+ provider: providerErrorLabelFromInstanceHint({ instanceId: String(desiredModelSelection.instanceId) }),
83823
+ method: "thread.turn.start",
83824
+ detail: `Requested provider instance '${desiredInstanceId}' is not configured in this build.`
83825
+ })));
83826
+ const desiredDriverKind = desiredInfo.driverKind;
83827
+ if (!isProviderDriverKind(desiredDriverKind)) return yield* new ProviderAdapterRequestError({
83828
+ provider: providerErrorLabel(String(desiredDriverKind)),
83829
+ method: "thread.turn.start",
83830
+ detail: `Requested provider instance '${desiredInstanceId}' uses unknown provider driver '${desiredDriverKind}'. The driver is not installed in this build.`
83831
+ });
83832
+ const preferredProvider = desiredDriverKind;
83833
+ if (options?.pendingTurnStart === true && thread.session?.status !== "running") yield* setThreadSession({
83834
+ threadId,
83339
83835
  session: {
83340
- threadId: thread.id,
83341
- status: "stopped",
83342
- providerName: thread.session?.providerName ?? null,
83343
- ...thread.session?.providerInstanceId !== void 0 ? { providerInstanceId: thread.session.providerInstanceId } : {},
83344
- runtimeMode: thread.session?.runtimeMode ?? DEFAULT_RUNTIME_MODE,
83836
+ threadId,
83837
+ status: "starting",
83838
+ providerName: activeSession?.provider ?? preferredProvider,
83839
+ providerInstanceId: activeSession?.providerInstanceId ?? desiredInstanceId,
83840
+ runtimeMode: desiredRuntimeMode,
83345
83841
  activeTurnId: null,
83346
- lastError: thread.session?.lastError ?? null,
83347
- updatedAt: now
83842
+ lastError: null,
83843
+ updatedAt: createdAt
83348
83844
  },
83349
- createdAt: now
83845
+ createdAt
83350
83846
  });
83351
- });
83352
- const processDomainEvent = Effect.fn("processDomainEvent")(function* (event) {
83353
- yield* Effect.annotateCurrentSpan({
83354
- "orchestration.event_type": event.type,
83355
- "orchestration.thread_id": event.payload.threadId,
83356
- ...event.commandId ? { "orchestration.command_id": event.commandId } : {}
83847
+ if (thread.session !== null) yield* rejectStartedThreadModelChangeIfRequired({
83848
+ threadId,
83849
+ currentModelSelection: activeSession?.model !== void 0 ? {
83850
+ ...thread.modelSelection,
83851
+ instanceId: currentInstanceId,
83852
+ model: activeSession.model
83853
+ } : thread.modelSelection,
83854
+ requestedModelSelection
83357
83855
  });
83358
- yield* increment(orchestrationEventsProcessedTotal, { eventType: event.type });
83359
- switch (event.type) {
83360
- case "thread.runtime-mode-set": {
83361
- const thread = yield* resolveThread(event.payload.threadId);
83362
- if (!thread?.session || thread.session.status === "stopped") return;
83363
- const cachedModelSelection = threadModelSelections.get(event.payload.threadId);
83364
- yield* ensureSessionForThread(event.payload.threadId, event.occurredAt, cachedModelSelection !== void 0 ? { modelSelection: cachedModelSelection } : {});
83365
- return;
83366
- }
83367
- case "thread.turn-start-requested":
83368
- yield* processTurnStartRequested(event);
83369
- return;
83370
- case "thread.turn-interrupt-requested":
83371
- yield* processTurnInterruptRequested(event);
83372
- return;
83373
- case "thread.approval-response-requested":
83374
- yield* processApprovalResponseRequested(event);
83375
- return;
83376
- case "thread.user-input-response-requested":
83377
- yield* processUserInputResponseRequested(event);
83378
- return;
83379
- case "thread.session-stop-requested":
83380
- yield* processSessionStopRequested(event);
83381
- return;
83856
+ if (thread.session !== null && requestedModelSelection !== void 0 && requestedModelSelection.instanceId !== currentInstanceId) {
83857
+ if (currentInfo.driverKind !== desiredInfo.driverKind) return yield* new ProviderAdapterRequestError({
83858
+ provider: preferredProvider,
83859
+ method: "thread.turn.start",
83860
+ detail: `Thread '${threadId}' is bound to driver '${currentInfo.driverKind}' and cannot switch to '${desiredInfo.driverKind}'.`
83861
+ });
83862
+ if (currentInfo.continuationIdentity.continuationKey !== desiredInfo.continuationIdentity.continuationKey) return yield* new ProviderAdapterRequestError({
83863
+ provider: preferredProvider,
83864
+ method: "thread.turn.start",
83865
+ detail: `Thread '${threadId}' cannot switch from instance '${currentInstanceId}' to '${desiredInstanceId}' because their provider resume state is incompatible.`
83866
+ });
83382
83867
  }
83383
- });
83384
- const processDomainEventSafely = (event) => processDomainEvent(event).pipe(Effect.catchCause((cause) => {
83385
- if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause);
83386
- return Effect.logWarning("provider command reactor failed to process event", {
83387
- eventType: event.type,
83388
- cause: Cause.pretty(cause)
83868
+ const project = yield* resolveProject(thread.projectId);
83869
+ const effectiveCwd = resolveThreadWorkspaceCwd({
83870
+ thread,
83871
+ projects: project ? [project] : []
83389
83872
  });
83390
- }));
83391
- const worker = yield* makeDrainableWorker(processDomainEventSafely);
83392
- return {
83393
- start: Effect.fn("start")(function* () {
83394
- const processEvent = Effect.fn("processEvent")(function* (event) {
83395
- if (event.type === "thread.runtime-mode-set" || event.type === "thread.turn-start-requested" || event.type === "thread.turn-interrupt-requested" || event.type === "thread.approval-response-requested" || event.type === "thread.user-input-response-requested" || event.type === "thread.session-stop-requested") return yield* worker.enqueue(event);
83873
+ const startProviderSession = (input) => providerService.startSession(threadId, {
83874
+ threadId,
83875
+ ...preferredProvider ? { provider: preferredProvider } : {},
83876
+ providerInstanceId: desiredInstanceId,
83877
+ ...effectiveCwd ? { cwd: effectiveCwd } : {},
83878
+ modelSelection: desiredModelSelection,
83879
+ ...input?.resumeCursor !== void 0 ? { resumeCursor: input.resumeCursor } : {},
83880
+ runtimeMode: desiredRuntimeMode
83881
+ });
83882
+ const bindSessionToThread = (session) => Effect.gen(function* () {
83883
+ if (session.providerInstanceId === void 0) return yield* new ProviderAdapterRequestError({
83884
+ provider: providerErrorLabel(session.provider),
83885
+ method: "thread.turn.start",
83886
+ detail: `Provider session '${session.threadId}' started without a provider instance id.`
83887
+ });
83888
+ yield* setThreadSession({
83889
+ threadId,
83890
+ session: {
83891
+ threadId,
83892
+ status: options?.pendingTurnStart === true && session.status === "ready" ? "starting" : mapProviderSessionStatusToOrchestrationStatus(session.status),
83893
+ providerName: session.provider,
83894
+ providerInstanceId: session.providerInstanceId,
83895
+ runtimeMode: desiredRuntimeMode,
83896
+ activeTurnId: null,
83897
+ lastError: session.lastError ?? null,
83898
+ updatedAt: session.updatedAt
83899
+ },
83900
+ createdAt
83396
83901
  });
83397
- yield* Effect.forkScoped(Stream.runForEach(orchestrationEngine.streamDomainEvents, processEvent));
83398
- }),
83399
- drain: worker.drain
83400
- };
83401
- });
83402
- const ProviderCommandReactorLive = Layer.effect(ProviderCommandReactor, make$2);
83403
- (() => {
83404
- try {
83405
- return process.env.NODE_ENV === "development";
83406
- } catch {
83407
- return false;
83408
- }
83409
- })();
83410
- const COMMIT_METADATA_SPLIT = /(?=^From [a-f0-9]+ .+$)/m;
83411
- const GIT_DIFF_FILE_BREAK_REGEX = /(?=^diff --git)/gm;
83412
- const FILENAME_HEADER_REGEX = /^(---|\+\+\+)\s+([^\t\r\n]+)/;
83413
- const FILENAME_HEADER_REGEX_GIT = /^(---|\+\+\+)\s+[ab]\/([^\t\r\n]+)/;
83414
- const ALTERNATE_FILE_NAMES_GIT = /^diff --git (?:"a\/(.+?)"|a\/(.+?)) (?:"b\/(.+?)"|b\/(.+?))$/;
83415
- const INDEX_LINE_METADATA = /^index ([0-9a-f]+)\.\.([0-9a-f]+)(?: (\d+))?$/i;
83416
- Object.freeze({
83417
- fromStart: 0,
83418
- fromEnd: 0
83419
- });
83420
- //#endregion
83421
- //#region ../../node_modules/.pnpm/@pierre+diffs@1.3.0-beta.5_patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b3_3b51331382fbad9d080c5033ee1e2a6a/node_modules/@pierre/diffs/dist/utils/cleanLastNewline.js
83422
- function cleanLastNewline(contents) {
83423
- let end = contents.length;
83424
- if (contents.charCodeAt(end - 1) === 10) {
83425
- end--;
83426
- if (contents.charCodeAt(end - 1) === 13) end--;
83427
- }
83428
- return contents.slice(0, end);
83429
- }
83430
- //#endregion
83431
- //#region ../../node_modules/.pnpm/@pierre+diffs@1.3.0-beta.5_patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b3_3b51331382fbad9d080c5033ee1e2a6a/node_modules/@pierre/diffs/dist/utils/detachString.js
83432
- const stringDetachEncoder = new TextEncoder();
83433
- const stringDetachDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
83434
- const SURROGATE_CODE_UNIT_PATTERN = /[\uD800-\uDFFF]/;
83435
- const STRING_DETACH_INITIAL_BUFFER_SIZE = 1024;
83436
- let stringDetachBuffer = new Uint8Array(STRING_DETACH_INITIAL_BUFFER_SIZE);
83437
- function releaseStringDetachBuffer() {
83438
- if (stringDetachBuffer.length !== STRING_DETACH_INITIAL_BUFFER_SIZE) stringDetachBuffer = new Uint8Array(STRING_DETACH_INITIAL_BUFFER_SIZE);
83439
- }
83440
- function detachString(value) {
83441
- if (value.length === 0) return value;
83442
- if (SURROGATE_CODE_UNIT_PATTERN.test(value)) return JSON.parse(JSON.stringify(value));
83443
- const requiredByteLength = value.length * 3;
83444
- if (stringDetachBuffer.length < requiredByteLength) stringDetachBuffer = new Uint8Array(requiredByteLength);
83445
- const { written } = stringDetachEncoder.encodeInto(value, stringDetachBuffer);
83446
- return stringDetachDecoder.decode(stringDetachBuffer.subarray(0, written));
83447
- }
83448
- //#endregion
83449
- //#region ../../node_modules/.pnpm/@pierre+diffs@1.3.0-beta.5_patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b3_3b51331382fbad9d080c5033ee1e2a6a/node_modules/@pierre/diffs/dist/utils/parsePatchFiles.js
83450
- function processPatch(data, cacheKeyPrefix, throwOnError) {
83451
- try {
83452
- return _processPatch(data, cacheKeyPrefix, throwOnError);
83453
- } finally {
83454
- releaseStringDetachBuffer();
83455
- }
83456
- }
83457
- function _processPatch(data, cacheKeyPrefix, throwOnError = false) {
83458
- const isGitDiff = isGitDiffPatch(data);
83459
- const rawFiles = isGitDiff ? splitGitDiffFiles(data) : splitUnifiedDiffFiles(data);
83460
- let patchMetadata;
83461
- const files = [];
83462
- for (const fileOrPatchMetadata of rawFiles) {
83463
- if (isGitDiff && !GIT_DIFF_FILE_BREAK_REGEX.test(fileOrPatchMetadata)) {
83464
- if (patchMetadata == null) patchMetadata = detachString(fileOrPatchMetadata);
83465
- else if (throwOnError) throw Error("parsePatchContent: unknown file blob");
83466
- else console.error("parsePatchContent: unknown file blob:", fileOrPatchMetadata);
83467
- continue;
83468
- } else if (!isGitDiff && !startsWithUnifiedDiffFileHeader(fileOrPatchMetadata)) {
83469
- if (patchMetadata == null) patchMetadata = detachString(fileOrPatchMetadata);
83470
- else if (throwOnError) throw Error("parsePatchContent: unknown file blob");
83471
- else console.error("parsePatchContent: unknown file blob:", fileOrPatchMetadata);
83472
- continue;
83473
- }
83474
- const currentFile = _processFile(fileOrPatchMetadata, {
83475
- cacheKey: cacheKeyPrefix != null ? `${cacheKeyPrefix}-${files.length}` : void 0,
83476
- isGitDiff,
83477
- throwOnError
83478
83902
  });
83479
- if (currentFile != null) files.push(currentFile);
83480
- }
83481
- return {
83482
- patchMetadata,
83483
- files
83484
- };
83485
- }
83486
- function _processFile(fileDiffString, { cacheKey, isGitDiff = GIT_DIFF_FILE_BREAK_REGEX.test(fileDiffString), oldFile, newFile, throwOnError = false } = {}) {
83487
- let lastHunkEnd = 0;
83488
- const hunks = splitAtLinePrefix(fileDiffString, "@@ ");
83489
- let currentFile;
83490
- const isPartial = oldFile == null || newFile == null;
83491
- let deletionLineIndex = 0;
83492
- let additionLineIndex = 0;
83493
- for (const hunk of hunks) {
83494
- const lines = splitWithNewlines(hunk);
83495
- const firstLine = lines[0];
83496
- if (firstLine == null) {
83497
- if (throwOnError) throw Error("parsePatchContent: invalid hunk");
83498
- else console.error("parsePatchContent: invalid hunk", hunk);
83499
- continue;
83500
- }
83501
- const fileHeader = parseHunkHeader(firstLine);
83502
- let additionLines = 0;
83503
- let deletionLines = 0;
83504
- if (fileHeader == null || currentFile == null) {
83505
- if (currentFile != null) {
83506
- if (throwOnError) throw Error("parsePatchContent: Invalid hunk");
83507
- else console.error("parsePatchContent: Invalid hunk", hunk);
83508
- continue;
83509
- }
83510
- currentFile = {
83511
- name: "",
83512
- type: "change",
83513
- hunks: [],
83514
- splitLineCount: 0,
83515
- unifiedLineCount: 0,
83516
- isPartial,
83517
- additionLines: !isPartial && oldFile != null && newFile != null ? splitFileContents(newFile.contents) : [],
83518
- deletionLines: !isPartial && oldFile != null && newFile != null ? splitFileContents(oldFile.contents) : [],
83519
- cacheKey: maybeDetachOptionalString(cacheKey)
83520
- };
83521
- if (currentFile.additionLines.length === 1 && newFile?.contents === "") currentFile.additionLines.length = 0;
83522
- if (currentFile.deletionLines.length === 1 && oldFile?.contents === "") currentFile.deletionLines.length = 0;
83523
- for (const line of lines) {
83524
- if (line.startsWith("diff --git")) {
83525
- const filenameMatch$1 = line.trim().match(ALTERNATE_FILE_NAMES_GIT);
83526
- const prevName = filenameMatch$1?.[1] ?? filenameMatch$1?.[2];
83527
- const name = filenameMatch$1?.[3] ?? filenameMatch$1?.[4];
83528
- if (prevName == null || name == null) {
83529
- if (throwOnError) throw Error("parsePatchContent: invalid git diff header");
83530
- else console.error("parsePatchContent: invalid git diff header", line);
83531
- continue;
83532
- }
83533
- currentFile.name = detachString(name.trim());
83534
- if (prevName !== name) currentFile.prevName = detachString(prevName.trim());
83535
- continue;
83536
- }
83537
- const filenameMatch = line.startsWith("---") || line.startsWith("+++") ? line.match(isGitDiff ? FILENAME_HEADER_REGEX_GIT : FILENAME_HEADER_REGEX) : null;
83538
- if (filenameMatch != null) {
83539
- const [, type, fileName] = filenameMatch;
83540
- if (type === "---" && fileName !== "/dev/null") {
83541
- const detachedFileName = detachString(fileName.trim());
83542
- currentFile.prevName = detachedFileName;
83543
- currentFile.name = detachedFileName;
83544
- } else if (type === "+++" && fileName !== "/dev/null") currentFile.name = detachString(fileName.trim());
83545
- } else if (isGitDiff) {
83546
- if (line.startsWith("new mode ")) currentFile.mode = detachString(line.slice(8).trim());
83547
- if (line.startsWith("old mode ")) currentFile.prevMode = detachString(line.slice(8).trim());
83548
- if (line.startsWith("new file mode")) {
83549
- currentFile.type = "new";
83550
- currentFile.mode = detachString(line.slice(13).trim());
83551
- }
83552
- if (line.startsWith("deleted file mode")) {
83553
- currentFile.type = "deleted";
83554
- currentFile.mode = detachString(line.slice(17).trim());
83555
- }
83556
- if (line.startsWith("similarity index")) if (line.startsWith("similarity index 100%")) currentFile.type = "rename-pure";
83557
- else currentFile.type = "rename-changed";
83558
- if (line.startsWith("index ")) {
83559
- const [, prevObjectId, newObjectId, mode] = line.trim().match(INDEX_LINE_METADATA) ?? [];
83560
- if (prevObjectId != null) currentFile.prevObjectId = detachString(prevObjectId);
83561
- if (newObjectId != null) currentFile.newObjectId = detachString(newObjectId);
83562
- if (mode != null) currentFile.mode = detachString(mode);
83563
- }
83564
- if (line.startsWith("rename from ")) currentFile.prevName = detachString(line.slice(12).trim());
83565
- if (line.startsWith("rename to ")) currentFile.name = detachString(line.slice(10).trim());
83566
- }
83567
- }
83568
- continue;
83903
+ const existingSessionThreadId = thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null;
83904
+ if (existingSessionThreadId) {
83905
+ const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode;
83906
+ const cwdChanged = effectiveCwd !== activeSession?.cwd;
83907
+ const sessionModelSwitch = (yield* providerService.getCapabilities(desiredInstanceId)).sessionModelSwitch;
83908
+ const modelChanged = requestedModelSelection !== void 0 && requestedModelSelection.model !== activeSession?.model;
83909
+ const instanceChanged = requestedModelSelection !== void 0 && activeSession?.providerInstanceId !== requestedModelSelection.instanceId;
83910
+ const shouldRestartForModelChange = modelChanged && sessionModelSwitch === "unsupported";
83911
+ const previousModelSelection = threadModelSelections.get(threadId);
83912
+ const shouldRestartForModelSelectionChange = preferredProvider === "claudeAgent" && requestedModelSelection !== void 0 && !Equal.equals(previousModelSelection, requestedModelSelection);
83913
+ if (!runtimeModeChanged && !cwdChanged && !instanceChanged && !shouldRestartForModelChange && !shouldRestartForModelSelectionChange) return existingSessionThreadId;
83914
+ const resumeCursor = shouldRestartForModelChange ? void 0 : activeSession?.resumeCursor ?? void 0;
83915
+ yield* Effect.logInfo("provider command reactor restarting provider session", {
83916
+ threadId,
83917
+ existingSessionThreadId,
83918
+ currentProvider: activeSession?.provider,
83919
+ currentInstanceId,
83920
+ desiredInstanceId,
83921
+ desiredProvider: desiredModelSelection.instanceId,
83922
+ currentRuntimeMode: thread.session?.runtimeMode,
83923
+ desiredRuntimeMode: thread.runtimeMode,
83924
+ runtimeModeChanged,
83925
+ previousCwd: activeSession?.cwd,
83926
+ desiredCwd: effectiveCwd,
83927
+ cwdChanged,
83928
+ modelChanged,
83929
+ instanceChanged,
83930
+ shouldRestartForModelChange,
83931
+ shouldRestartForModelSelectionChange,
83932
+ hasResumeCursor: resumeCursor !== void 0
83933
+ });
83934
+ const restartedSession = yield* startProviderSession(resumeCursor !== void 0 ? { resumeCursor } : void 0);
83935
+ yield* Effect.logInfo("provider command reactor restarted provider session", {
83936
+ threadId,
83937
+ previousSessionId: existingSessionThreadId,
83938
+ restartedSessionThreadId: restartedSession.threadId,
83939
+ provider: restartedSession.provider,
83940
+ runtimeMode: restartedSession.runtimeMode,
83941
+ cwd: restartedSession.cwd
83942
+ });
83943
+ yield* bindSessionToThread(restartedSession);
83944
+ return restartedSession.threadId;
83569
83945
  }
83570
- let currentContent;
83571
- let lastLineType;
83572
- while (lines.length > 0 && (lines[lines.length - 1] === "\n" || lines[lines.length - 1] === "\r" || lines[lines.length - 1] === "\r\n" || lines[lines.length - 1] === "")) lines.pop();
83573
- const { additionStart, deletionStart } = fileHeader;
83574
- deletionLineIndex = isPartial ? deletionLineIndex : deletionStart - 1;
83575
- additionLineIndex = isPartial ? additionLineIndex : additionStart - 1;
83576
- const hunkData = {
83577
- collapsedBefore: 0,
83578
- splitLineCount: 0,
83579
- splitLineStart: 0,
83580
- unifiedLineCount: 0,
83581
- unifiedLineStart: 0,
83582
- additionCount: fileHeader.additionCount,
83583
- additionStart,
83584
- additionLines,
83585
- deletionCount: fileHeader.deletionCount,
83586
- deletionStart,
83587
- deletionLines,
83588
- deletionLineIndex,
83589
- additionLineIndex,
83590
- hunkContent: [],
83591
- hunkContext: maybeDetachOptionalString(fileHeader.hunkContext),
83592
- hunkSpecs: detachString(firstLine),
83593
- noEOFCRAdditions: false,
83594
- noEOFCRDeletions: false
83946
+ const startedSession = yield* startProviderSession(void 0);
83947
+ yield* bindSessionToThread(startedSession);
83948
+ return startedSession.threadId;
83949
+ });
83950
+ const buildSendTurnRequestForThread = Effect.fnUntraced(function* (input) {
83951
+ const thread = yield* resolveThread(input.threadId);
83952
+ if (!thread) return yield* Effect.die(/* @__PURE__ */ new Error(`Thread '${input.threadId}' was not found in read model.`));
83953
+ yield* ensureSessionForThread(input.threadId, input.createdAt, {
83954
+ ...input.modelSelection !== void 0 ? { modelSelection: input.modelSelection } : {},
83955
+ pendingTurnStart: true
83956
+ });
83957
+ if (input.modelSelection !== void 0) threadModelSelections.set(input.threadId, input.modelSelection);
83958
+ const normalizedInput = toNonEmptyProviderInput(input.messageText);
83959
+ const normalizedAttachments = input.attachments ?? [];
83960
+ const activeSession = yield* providerService.listSessions().pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === input.threadId)));
83961
+ const sessionModelSwitch = activeSession === void 0 ? "in-session" : activeSession.providerInstanceId === void 0 ? yield* new ProviderAdapterRequestError({
83962
+ provider: providerErrorLabel(activeSession.provider),
83963
+ method: "thread.turn.start",
83964
+ detail: `Active provider session '${activeSession.threadId}' is missing a provider instance id.`
83965
+ }) : (yield* providerService.getCapabilities(activeSession.providerInstanceId)).sessionModelSwitch;
83966
+ const requestedModelSelection = input.modelSelection ?? threadModelSelections.get(input.threadId) ?? thread.modelSelection;
83967
+ const modelForTurn = sessionModelSwitch === "unsupported" && input.modelSelection === void 0 ? activeSession?.model !== void 0 ? {
83968
+ ...requestedModelSelection,
83969
+ model: activeSession.model
83970
+ } : requestedModelSelection : input.modelSelection;
83971
+ return {
83972
+ threadId: input.threadId,
83973
+ ...normalizedInput ? { input: normalizedInput } : {},
83974
+ ...normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {},
83975
+ ...modelForTurn !== void 0 ? { modelSelection: modelForTurn } : {},
83976
+ ...input.interactionMode !== void 0 ? { interactionMode: input.interactionMode } : {}
83595
83977
  };
83596
- let parsedAdditionLines = 0;
83597
- let parsedDeletionLines = 0;
83598
- for (let lineIndex = 1; lineIndex < lines.length; lineIndex++) {
83599
- const rawLine = lines[lineIndex];
83600
- if (parsedAdditionLines >= hunkData.additionCount && parsedDeletionLines >= hunkData.deletionCount && !rawLine.startsWith("\\")) {
83601
- if (throwOnError && isHunkBodyLine(rawLine) && !isFormatPatchVersionSeparator(rawLine)) throw Error("parsePatchContent: hunk has more lines than expected");
83602
- break;
83603
- }
83604
- const firstChar = rawLine[0];
83605
- if (firstChar !== "+" && firstChar !== "-" && firstChar !== " " && firstChar !== "\\") {
83606
- if (throwOnError) throw Error("parsePatchContent: invalid hunk line");
83607
- console.error(`parseLineType: Invalid firstChar: "${firstChar}", full line: "${rawLine}"`);
83608
- console.error("processFile: invalid rawLine:", rawLine);
83609
- continue;
83610
- }
83611
- const type = parseRawLineType(firstChar);
83612
- if (type === "addition") {
83613
- if (throwOnError && parsedAdditionLines >= hunkData.additionCount) throw Error("parsePatchContent: hunk has too many addition lines");
83614
- const line = getParsedLineContent(rawLine);
83615
- if (currentContent == null || currentContent.type !== "change") {
83616
- currentContent = createContentGroup("change", deletionLineIndex, additionLineIndex);
83617
- hunkData.hunkContent.push(currentContent);
83618
- }
83619
- additionLineIndex++;
83620
- parsedAdditionLines++;
83621
- if (isPartial) currentFile.additionLines.push(line);
83622
- currentContent.additions++;
83623
- additionLines++;
83624
- lastLineType = "addition";
83625
- } else if (type === "deletion") {
83626
- if (throwOnError && parsedDeletionLines >= hunkData.deletionCount) throw Error("parsePatchContent: hunk has too many deletion lines");
83627
- const line = getParsedLineContent(rawLine);
83628
- if (currentContent == null || currentContent.type !== "change") {
83629
- currentContent = createContentGroup("change", deletionLineIndex, additionLineIndex);
83630
- hunkData.hunkContent.push(currentContent);
83631
- }
83632
- deletionLineIndex++;
83633
- parsedDeletionLines++;
83634
- if (isPartial) currentFile.deletionLines.push(line);
83635
- currentContent.deletions++;
83636
- deletionLines++;
83637
- lastLineType = "deletion";
83638
- } else if (type === "context") {
83639
- if (throwOnError && (parsedDeletionLines >= hunkData.deletionCount || parsedAdditionLines >= hunkData.additionCount)) throw Error("parsePatchContent: hunk has too many context lines");
83640
- const line = getParsedLineContent(rawLine);
83641
- if (currentContent == null || currentContent.type !== "context") {
83642
- currentContent = createContentGroup("context", deletionLineIndex, additionLineIndex);
83643
- hunkData.hunkContent.push(currentContent);
83644
- }
83645
- additionLineIndex++;
83646
- deletionLineIndex++;
83647
- parsedAdditionLines++;
83648
- parsedDeletionLines++;
83649
- if (isPartial) {
83650
- currentFile.deletionLines.push(line);
83651
- currentFile.additionLines.push(line);
83652
- }
83653
- currentContent.lines++;
83654
- lastLineType = "context";
83655
- } else if (type === "metadata" && currentContent != null) {
83656
- if (currentContent.type === "context") {
83657
- hunkData.noEOFCRAdditions = true;
83658
- hunkData.noEOFCRDeletions = true;
83659
- } else if (lastLineType === "deletion") hunkData.noEOFCRDeletions = true;
83660
- else if (lastLineType === "addition") hunkData.noEOFCRAdditions = true;
83661
- if (isPartial && (lastLineType === "addition" || lastLineType === "context")) {
83662
- const lastIndex = currentFile.additionLines.length - 1;
83663
- if (lastIndex >= 0) currentFile.additionLines[lastIndex] = cleanLastNewline(currentFile.additionLines[lastIndex]);
83664
- }
83665
- if (isPartial && (lastLineType === "deletion" || lastLineType === "context")) {
83666
- const lastIndex = currentFile.deletionLines.length - 1;
83667
- if (lastIndex >= 0) currentFile.deletionLines[lastIndex] = cleanLastNewline(currentFile.deletionLines[lastIndex]);
83668
- }
83669
- }
83978
+ });
83979
+ const maybeGenerateAndRenameWorktreeBranchForFirstTurn = Effect.fn("maybeGenerateAndRenameWorktreeBranchForFirstTurn")(function* (input) {
83980
+ if (!input.branch || !input.worktreePath) return;
83981
+ if (!isTemporaryWorktreeBranch(input.branch)) return;
83982
+ const oldBranch = input.branch;
83983
+ const cwd = input.worktreePath;
83984
+ const attachments = input.attachments ?? [];
83985
+ yield* Effect.gen(function* () {
83986
+ const settings = yield* serverSettingsService.getSettings;
83987
+ const modelSelection = settings.sourceControlWriterModelSelection === null ? settings.textGenerationModelSelection : resolveSourceControlWriterModelSelection(settings, yield* providerRegistry.getProviders);
83988
+ const generated = yield* textGeneration.generateBranchName({
83989
+ cwd,
83990
+ message: input.messageText,
83991
+ ...attachments.length > 0 ? { attachments } : {},
83992
+ modelSelection
83993
+ });
83994
+ if (!generated) return;
83995
+ const targetBranch = buildGeneratedWorktreeBranchName(generated.branch);
83996
+ if (targetBranch === oldBranch) return;
83997
+ const renamed = yield* gitWorkflow.renameBranch({
83998
+ cwd,
83999
+ oldBranch,
84000
+ newBranch: targetBranch
84001
+ });
84002
+ yield* orchestrationEngine.dispatch({
84003
+ type: "thread.meta.update",
84004
+ commandId: yield* serverCommandId("worktree-branch-rename"),
84005
+ threadId: input.threadId,
84006
+ branch: renamed.branch,
84007
+ worktreePath: cwd
84008
+ });
84009
+ yield* vcsStatusBroadcaster.refreshStatus(cwd).pipe(Effect.ignoreCause({ log: true }));
84010
+ }).pipe(Effect.catchCause((cause) => Effect.logWarning("provider command reactor failed to generate or rename worktree branch", {
84011
+ threadId: input.threadId,
84012
+ cwd,
84013
+ oldBranch,
84014
+ cause: Cause.pretty(cause)
84015
+ })));
84016
+ });
84017
+ const maybeGenerateThreadTitleForFirstTurn = Effect.fn("maybeGenerateThreadTitleForFirstTurn")(function* (input) {
84018
+ const attachments = input.attachments ?? [];
84019
+ yield* Effect.gen(function* () {
84020
+ const { textGenerationModelSelection: modelSelection } = yield* serverSettingsService.getSettings;
84021
+ const generated = yield* textGeneration.generateThreadTitle({
84022
+ cwd: input.cwd,
84023
+ message: input.messageText,
84024
+ ...attachments.length > 0 ? { attachments } : {},
84025
+ modelSelection
84026
+ });
84027
+ if (!generated) return;
84028
+ const thread = yield* resolveThread(input.threadId);
84029
+ if (!thread) return;
84030
+ if (!canReplaceThreadTitle(thread.title, input.titleSeed)) return;
84031
+ yield* orchestrationEngine.dispatch({
84032
+ type: "thread.meta.update",
84033
+ commandId: yield* serverCommandId("thread-title-rename"),
84034
+ threadId: input.threadId,
84035
+ title: generated.title
84036
+ });
84037
+ }).pipe(Effect.catchCause((cause) => Effect.logWarning("provider command reactor failed to generate or rename thread title", {
84038
+ threadId: input.threadId,
84039
+ cwd: input.cwd,
84040
+ cause: Cause.pretty(cause)
84041
+ })));
84042
+ });
84043
+ const processTurnStartRequested = Effect.fn("processTurnStartRequested")(function* (event) {
84044
+ const key = turnStartKeyForEvent(event);
84045
+ if (yield* hasHandledTurnStartRecently(key)) return;
84046
+ const thread = yield* resolveThread(event.payload.threadId);
84047
+ if (!thread) return;
84048
+ const message = thread.messages.find((entry) => entry.id === event.payload.messageId);
84049
+ if (!message || message.role !== "user") {
84050
+ yield* appendProviderFailureActivity({
84051
+ threadId: event.payload.threadId,
84052
+ kind: "provider.turn.start.failed",
84053
+ summary: "Provider turn start failed",
84054
+ detail: `User message '${event.payload.messageId}' was not found for turn start request.`,
84055
+ turnId: null,
84056
+ createdAt: event.payload.createdAt
84057
+ });
84058
+ return;
83670
84059
  }
83671
- if (throwOnError && (parsedAdditionLines !== hunkData.additionCount || parsedDeletionLines !== hunkData.deletionCount)) throw Error("parsePatchContent: hunk line count mismatch");
83672
- hunkData.additionLines = additionLines;
83673
- hunkData.deletionLines = deletionLines;
83674
- hunkData.collapsedBefore = Math.max(hunkData.additionStart - 1 - lastHunkEnd, 0);
83675
- currentFile.hunks.push(hunkData);
83676
- lastHunkEnd = hunkData.additionStart + hunkData.additionCount - 1;
83677
- for (const content of hunkData.hunkContent) if (content.type === "context") {
83678
- hunkData.splitLineCount += content.lines;
83679
- hunkData.unifiedLineCount += content.lines;
83680
- } else {
83681
- hunkData.splitLineCount += Math.max(content.additions, content.deletions);
83682
- hunkData.unifiedLineCount += content.deletions + content.additions;
84060
+ if (thread.messages.filter((entry) => entry.role === "user").length === 1) {
84061
+ const project = yield* resolveProject(thread.projectId);
84062
+ const generationCwd = resolveThreadWorkspaceCwd({
84063
+ thread,
84064
+ projects: project ? [project] : []
84065
+ }) ?? process.cwd();
84066
+ const generationInput = {
84067
+ messageText: message.text,
84068
+ ...message.attachments !== void 0 ? { attachments: message.attachments } : {},
84069
+ ...event.payload.titleSeed !== void 0 ? { titleSeed: event.payload.titleSeed } : {}
84070
+ };
84071
+ yield* maybeGenerateAndRenameWorktreeBranchForFirstTurn({
84072
+ threadId: event.payload.threadId,
84073
+ branch: thread.branch,
84074
+ worktreePath: thread.worktreePath,
84075
+ ...generationInput
84076
+ }).pipe(Effect.forkScoped);
84077
+ if (canReplaceThreadTitle(thread.title, event.payload.titleSeed)) yield* maybeGenerateThreadTitleForFirstTurn({
84078
+ threadId: event.payload.threadId,
84079
+ cwd: generationCwd,
84080
+ ...generationInput
84081
+ }).pipe(Effect.forkScoped);
83683
84082
  }
83684
- hunkData.splitLineStart = currentFile.splitLineCount + hunkData.collapsedBefore;
83685
- hunkData.unifiedLineStart = currentFile.unifiedLineCount + hunkData.collapsedBefore;
83686
- currentFile.splitLineCount += hunkData.collapsedBefore + hunkData.splitLineCount;
83687
- currentFile.unifiedLineCount += hunkData.collapsedBefore + hunkData.unifiedLineCount;
83688
- }
83689
- if (currentFile == null) return;
83690
- if (throwOnError && isPartial && !isGitDiff && currentFile.hunks.length === 0) throw Error("parsePatchContent: unified file has no hunks");
83691
- if (currentFile.hunks.length > 0 && !isPartial && currentFile.additionLines.length > 0 && currentFile.deletionLines.length > 0) {
83692
- const lastHunk = currentFile.hunks[currentFile.hunks.length - 1];
83693
- const lastHunkEnd$1 = lastHunk.additionStart + lastHunk.additionCount - 1;
83694
- const totalFileLines = currentFile.additionLines.length;
83695
- const collapsedAfter = Math.max(totalFileLines - lastHunkEnd$1, 0);
83696
- currentFile.splitLineCount += collapsedAfter;
83697
- currentFile.unifiedLineCount += collapsedAfter;
83698
- }
83699
- if (!isGitDiff) {
83700
- if (currentFile.prevName != null && currentFile.name !== currentFile.prevName) if (currentFile.hunks.length > 0) currentFile.type = "rename-changed";
83701
- else currentFile.type = "rename-pure";
83702
- else if ((oldFile == null || oldFile.contents === "") && newFile != null && newFile.contents !== "") currentFile.type = "new";
83703
- else if (oldFile != null && oldFile.contents !== "" && (newFile == null || newFile.contents === "")) currentFile.type = "deleted";
83704
- }
83705
- if (currentFile.type !== "rename-pure" && currentFile.type !== "rename-changed") currentFile.prevName = void 0;
83706
- return currentFile;
83707
- }
83708
- /**
83709
- * Parses a patch file string into an array of parsed patches.
83710
- *
83711
- * @param data - The raw patch file content (supports multi-commit patches)
83712
- * @param cacheKeyPrefix - Optional prefix for generating cache keys. When provided,
83713
- * each file in the patch will get a cache key in the format `prefix-patchIndex-fileIndex`.
83714
- * This enables caching of rendered diff results in the worker pool.
83715
- */
83716
- function parsePatchFiles(data, cacheKeyPrefix, throwOnError = false) {
83717
- const patches = [];
83718
- const rawPatches = hasCommitMetadataBoundary(data) ? data.split(COMMIT_METADATA_SPLIT) : [data];
83719
- for (const patch of rawPatches) try {
83720
- patches.push(processPatch(patch, cacheKeyPrefix != null ? `${cacheKeyPrefix}-${patches.length}` : void 0, throwOnError));
83721
- } catch (error) {
83722
- if (throwOnError) throw error;
83723
- else console.error(error);
83724
- }
83725
- return patches;
83726
- }
83727
- function hasCommitMetadataBoundary(data) {
83728
- return data.startsWith("From ") || data.includes("\nFrom ");
83729
- }
83730
- function splitFileContents(contents) {
83731
- const lines = splitWithNewlines(contents);
83732
- for (let index = 0; index < lines.length; index++) lines[index] = detachString(lines[index]);
83733
- return lines;
83734
- }
83735
- function splitWithNewlines(contents) {
83736
- if (contents.length === 0) return [""];
83737
- const lines = [];
83738
- let startIndex = 0;
83739
- for (;;) {
83740
- const newlineIndex = contents.indexOf("\n", startIndex);
83741
- if (newlineIndex === -1) break;
83742
- lines.push(contents.slice(startIndex, newlineIndex + 1));
83743
- startIndex = newlineIndex + 1;
83744
- }
83745
- if (startIndex < contents.length) lines.push(contents.slice(startIndex));
83746
- return lines;
83747
- }
83748
- function splitGitDiffFiles(contents) {
83749
- return splitAtLinePrefix(contents, "diff --git");
83750
- }
83751
- function splitUnifiedDiffFiles(contents) {
83752
- if (contents.length === 0) return [""];
83753
- const parts = [];
83754
- let partStartIndex = 0;
83755
- let lineStartIndex = 0;
83756
- let remainingDeletionLines = 0;
83757
- let remainingAdditionLines = 0;
83758
- let hasOpenedUnifiedFile = false;
83759
- while (lineStartIndex < contents.length) {
83760
- const nextLineStartIndex = getNextLineStartIndex(contents, lineStartIndex);
83761
- if (remainingDeletionLines <= 0 && remainingAdditionLines <= 0) {
83762
- if (isUnifiedDiffFileHeaderAt(contents, lineStartIndex)) {
83763
- if (lineStartIndex > partStartIndex) parts.push(contents.slice(partStartIndex, lineStartIndex));
83764
- partStartIndex = lineStartIndex;
83765
- hasOpenedUnifiedFile = true;
83766
- lineStartIndex = getNextLineStartIndex(contents, nextLineStartIndex);
83767
- continue;
83768
- }
83769
- if (hasOpenedUnifiedFile && contents.startsWith("@@ -", lineStartIndex)) {
83770
- const fileHeader = parseHunkHeader(contents.slice(lineStartIndex, nextLineStartIndex));
83771
- if (fileHeader != null) {
83772
- remainingDeletionLines = fileHeader.deletionCount;
83773
- remainingAdditionLines = fileHeader.additionCount;
83774
- }
84083
+ const handleTurnStartFailure = (cause) => {
84084
+ if (Cause.hasInterruptsOnly(cause)) return Effect.void;
84085
+ const detail = formatFailureDetail(cause);
84086
+ return setThreadSessionErrorOnTurnStartFailure({
84087
+ threadId: event.payload.threadId,
84088
+ detail,
84089
+ createdAt: event.payload.createdAt
84090
+ }).pipe(Effect.flatMap(() => appendProviderFailureActivity({
84091
+ threadId: event.payload.threadId,
84092
+ kind: "provider.turn.start.failed",
84093
+ summary: "Provider turn start failed",
84094
+ detail,
84095
+ turnId: null,
84096
+ createdAt: event.payload.createdAt
84097
+ })), Effect.asVoid);
84098
+ };
84099
+ const recoverTurnStartFailure = (cause) => handleTurnStartFailure(cause).pipe(Effect.catchCause((recoveryCause) => Effect.logWarning("provider command reactor failed to recover turn start failure", {
84100
+ eventType: event.type,
84101
+ threadId: event.payload.threadId,
84102
+ cause: Cause.pretty(recoveryCause),
84103
+ originalCause: Cause.pretty(cause)
84104
+ })));
84105
+ const sendTurnRequest = yield* buildSendTurnRequestForThread({
84106
+ threadId: event.payload.threadId,
84107
+ messageText: message.text,
84108
+ ...message.attachments !== void 0 ? { attachments: message.attachments } : {},
84109
+ ...event.payload.modelSelection !== void 0 ? { modelSelection: event.payload.modelSelection } : {},
84110
+ interactionMode: event.payload.interactionMode,
84111
+ createdAt: event.payload.createdAt
84112
+ }).pipe(Effect.map(Option.some), Effect.catchCause((cause) => handleTurnStartFailure(cause).pipe(Effect.as(Option.none()))));
84113
+ if (Option.isNone(sendTurnRequest)) return;
84114
+ yield* providerService.sendTurn(sendTurnRequest.value).pipe(Effect.catchCause(recoverTurnStartFailure), Effect.forkScoped);
84115
+ });
84116
+ const processTurnInterruptRequested = Effect.fn("processTurnInterruptRequested")(function* (event) {
84117
+ const thread = yield* resolveThread(event.payload.threadId);
84118
+ if (!thread) return;
84119
+ if (!(thread.session && thread.session.status !== "stopped")) return yield* appendProviderFailureActivity({
84120
+ threadId: event.payload.threadId,
84121
+ kind: "provider.turn.interrupt.failed",
84122
+ summary: "Provider turn interrupt failed",
84123
+ detail: "No active provider session is bound to this thread.",
84124
+ turnId: event.payload.turnId ?? null,
84125
+ createdAt: event.payload.createdAt
84126
+ });
84127
+ yield* providerService.interruptTurn({ threadId: event.payload.threadId });
84128
+ yield* appendTurnInterruptedActivity({
84129
+ threadId: event.payload.threadId,
84130
+ turnId: event.payload.turnId ?? null,
84131
+ createdAt: event.payload.createdAt
84132
+ });
84133
+ });
84134
+ const processApprovalResponseRequested = Effect.fn("processApprovalResponseRequested")(function* (event) {
84135
+ const thread = yield* resolveThread(event.payload.threadId);
84136
+ if (!thread) return;
84137
+ if (!(thread.session && thread.session.status !== "stopped")) return yield* appendProviderFailureActivity({
84138
+ threadId: event.payload.threadId,
84139
+ kind: "provider.approval.respond.failed",
84140
+ summary: "Provider approval response failed",
84141
+ detail: "No active provider session is bound to this thread.",
84142
+ turnId: null,
84143
+ createdAt: event.payload.createdAt,
84144
+ requestId: event.payload.requestId
84145
+ });
84146
+ yield* providerService.respondToRequest({
84147
+ threadId: event.payload.threadId,
84148
+ requestId: event.payload.requestId,
84149
+ decision: event.payload.decision
84150
+ }).pipe(Effect.catchCause((cause) => appendProviderFailureActivity({
84151
+ threadId: event.payload.threadId,
84152
+ kind: "provider.approval.respond.failed",
84153
+ summary: "Provider approval response failed",
84154
+ detail: isUnknownPendingApprovalRequestError(cause) ? stalePendingRequestDetail("approval", event.payload.requestId) : Cause.pretty(cause),
84155
+ turnId: null,
84156
+ createdAt: event.payload.createdAt,
84157
+ requestId: event.payload.requestId
84158
+ })));
84159
+ });
84160
+ const processUserInputResponseRequested = Effect.fn("processUserInputResponseRequested")(function* (event) {
84161
+ const thread = yield* resolveThread(event.payload.threadId);
84162
+ if (!thread) return;
84163
+ if (!(thread.session && thread.session.status !== "stopped")) return yield* appendProviderFailureActivity({
84164
+ threadId: event.payload.threadId,
84165
+ kind: "provider.user-input.respond.failed",
84166
+ summary: "Provider user input response failed",
84167
+ detail: "No active provider session is bound to this thread.",
84168
+ turnId: null,
84169
+ createdAt: event.payload.createdAt,
84170
+ requestId: event.payload.requestId
84171
+ });
84172
+ yield* providerService.respondToUserInput({
84173
+ threadId: event.payload.threadId,
84174
+ requestId: event.payload.requestId,
84175
+ answers: event.payload.answers
84176
+ }).pipe(Effect.catchCause((cause) => appendProviderFailureActivity({
84177
+ threadId: event.payload.threadId,
84178
+ kind: "provider.user-input.respond.failed",
84179
+ summary: "Provider user input response failed",
84180
+ detail: isUnknownPendingUserInputRequestError(cause) ? stalePendingRequestDetail("user-input", event.payload.requestId) : Cause.pretty(cause),
84181
+ turnId: null,
84182
+ createdAt: event.payload.createdAt,
84183
+ requestId: event.payload.requestId
84184
+ })));
84185
+ });
84186
+ const processSessionStopRequested = Effect.fn("processSessionStopRequested")(function* (event) {
84187
+ const thread = yield* resolveThread(event.payload.threadId);
84188
+ if (!thread) return;
84189
+ const now = event.payload.createdAt;
84190
+ if (thread.session && thread.session.status !== "stopped") yield* providerService.stopSession({ threadId: thread.id });
84191
+ yield* setThreadSession({
84192
+ threadId: thread.id,
84193
+ session: {
84194
+ threadId: thread.id,
84195
+ status: "stopped",
84196
+ providerName: thread.session?.providerName ?? null,
84197
+ ...thread.session?.providerInstanceId !== void 0 ? { providerInstanceId: thread.session.providerInstanceId } : {},
84198
+ runtimeMode: thread.session?.runtimeMode ?? DEFAULT_RUNTIME_MODE,
84199
+ activeTurnId: null,
84200
+ lastError: thread.session?.lastError ?? null,
84201
+ updatedAt: now
84202
+ },
84203
+ createdAt: now
84204
+ });
84205
+ });
84206
+ const processDomainEvent = Effect.fn("processDomainEvent")(function* (event) {
84207
+ yield* Effect.annotateCurrentSpan({
84208
+ "orchestration.event_type": event.type,
84209
+ "orchestration.thread_id": event.payload.threadId,
84210
+ ...event.commandId ? { "orchestration.command_id": event.commandId } : {}
84211
+ });
84212
+ yield* increment(orchestrationEventsProcessedTotal, { eventType: event.type });
84213
+ switch (event.type) {
84214
+ case "thread.runtime-mode-set": {
84215
+ const thread = yield* resolveThread(event.payload.threadId);
84216
+ if (!thread?.session || thread.session.status === "stopped") return;
84217
+ const cachedModelSelection = threadModelSelections.get(event.payload.threadId);
84218
+ yield* ensureSessionForThread(event.payload.threadId, event.occurredAt, cachedModelSelection !== void 0 ? { modelSelection: cachedModelSelection } : {});
84219
+ return;
83775
84220
  }
83776
- lineStartIndex = nextLineStartIndex;
83777
- continue;
83778
- }
83779
- const firstChar = contents[lineStartIndex];
83780
- if (firstChar === "\\") {
83781
- lineStartIndex = nextLineStartIndex;
83782
- continue;
84221
+ case "thread.turn-start-requested":
84222
+ yield* processTurnStartRequested(event);
84223
+ return;
84224
+ case "thread.turn-interrupt-requested":
84225
+ yield* processTurnInterruptRequested(event);
84226
+ return;
84227
+ case "thread.approval-response-requested":
84228
+ yield* processApprovalResponseRequested(event);
84229
+ return;
84230
+ case "thread.user-input-response-requested":
84231
+ yield* processUserInputResponseRequested(event);
84232
+ return;
84233
+ case "thread.session-stop-requested":
84234
+ yield* processSessionStopRequested(event);
84235
+ return;
83783
84236
  }
83784
- if (firstChar === " ") {
83785
- remainingDeletionLines = Math.max(remainingDeletionLines - 1, 0);
83786
- remainingAdditionLines = Math.max(remainingAdditionLines - 1, 0);
83787
- } else if (firstChar === "-") remainingDeletionLines = Math.max(remainingDeletionLines - 1, 0);
83788
- else if (firstChar === "+") remainingAdditionLines = Math.max(remainingAdditionLines - 1, 0);
83789
- lineStartIndex = nextLineStartIndex;
83790
- }
83791
- parts.push(contents.slice(partStartIndex));
83792
- return parts;
83793
- }
83794
- function startsWithUnifiedDiffFileHeader(contents) {
83795
- return isUnifiedDiffFileHeaderAt(contents, 0);
83796
- }
83797
- function isUnifiedDiffFileHeaderAt(contents, lineStartIndex) {
83798
- const nextLineStartIndex = getNextLineStartIndex(contents, lineStartIndex);
83799
- return isUnifiedDiffHeaderLineAt(contents, lineStartIndex, "---") && isUnifiedDiffHeaderLineAt(contents, nextLineStartIndex, "+++");
83800
- }
83801
- function isUnifiedDiffHeaderLineAt(contents, lineStartIndex, prefix) {
83802
- if (!contents.startsWith(prefix, lineStartIndex)) return false;
83803
- const separator = contents[lineStartIndex + prefix.length];
83804
- if (separator !== " " && separator !== " ") return false;
83805
- for (let index = lineStartIndex + prefix.length + 1; index < contents.length; index++) {
83806
- const char = contents[index];
83807
- if (char === "\n" || char === "\r") break;
83808
- if (char !== " " && char !== " ") return true;
83809
- }
83810
- return false;
83811
- }
83812
- function getNextLineStartIndex(contents, lineStartIndex) {
83813
- const newlineIndex = contents.indexOf("\n", lineStartIndex);
83814
- return newlineIndex === -1 ? contents.length : newlineIndex + 1;
83815
- }
83816
- function isHunkBodyLine(line) {
83817
- const firstChar = line[0];
83818
- return firstChar === "+" || firstChar === "-" || firstChar === " ";
83819
- }
83820
- function isFormatPatchVersionSeparator(line) {
83821
- if (!line.startsWith("--")) return false;
83822
- for (let index = 2; index < line.length; index++) {
83823
- const char = line[index];
83824
- if (char !== " " && char !== " " && char !== "\n" && char !== "\r") return false;
83825
- }
83826
- return true;
83827
- }
83828
- function parseHunkHeader(line) {
83829
- if (!line.startsWith("@@ -")) return;
83830
- let index = 4;
83831
- const deletionStartResult = readPositiveInteger(line, index);
83832
- if (deletionStartResult == null) return;
83833
- const deletionStart = deletionStartResult.value;
83834
- index = deletionStartResult.endIndex;
83835
- let deletionCount = 1;
83836
- if (line[index] === ",") {
83837
- const deletionCountResult = readPositiveInteger(line, index + 1);
83838
- if (deletionCountResult == null) return;
83839
- deletionCount = deletionCountResult.value;
83840
- index = deletionCountResult.endIndex;
83841
- }
83842
- if (line[index] !== " " || line[index + 1] !== "+") return;
83843
- index += 2;
83844
- const additionStartResult = readPositiveInteger(line, index);
83845
- if (additionStartResult == null) return;
83846
- const additionStart = additionStartResult.value;
83847
- index = additionStartResult.endIndex;
83848
- let additionCount = 1;
83849
- if (line[index] === ",") {
83850
- const additionCountResult = readPositiveInteger(line, index + 1);
83851
- if (additionCountResult == null) return;
83852
- additionCount = additionCountResult.value;
83853
- index = additionCountResult.endIndex;
83854
- }
83855
- if (line[index] !== " " || line[index + 1] !== "@" || line[index + 2] !== "@") return;
83856
- let hunkContext;
83857
- const contextStartIndex = index + 3;
83858
- if (line[contextStartIndex] === " ") hunkContext = trimLineEnd(line.slice(contextStartIndex + 1));
83859
- return {
83860
- additionCount,
83861
- additionStart,
83862
- deletionCount,
83863
- deletionStart,
83864
- hunkContext
83865
- };
83866
- }
83867
- function readPositiveInteger(value, startIndex) {
83868
- let index = startIndex;
83869
- let parsedValue = 0;
83870
- for (; index < value.length; index++) {
83871
- const digit = value.charCodeAt(index) - 48;
83872
- if (digit < 0 || digit > 9) break;
83873
- parsedValue = parsedValue * 10 + digit;
83874
- }
83875
- if (index === startIndex) return;
83876
- return {
83877
- value: parsedValue,
83878
- endIndex: index
83879
- };
83880
- }
83881
- function trimLineEnd(value) {
83882
- if (value.endsWith("\r\n")) return value.slice(0, -2);
83883
- if (value.endsWith("\n")) return value.slice(0, -1);
83884
- return value;
83885
- }
83886
- function isGitDiffPatch(data) {
83887
- return data.startsWith("diff --git") || data.includes("\ndiff --git");
83888
- }
83889
- function splitAtLinePrefix(contents, prefix) {
83890
- if (contents.length === 0) return [""];
83891
- const newlinePrefix = `\n${prefix}`;
83892
- const firstBoundaryIndex = contents.startsWith(prefix) ? 0 : findLinePrefixIndex(contents, newlinePrefix, 0);
83893
- if (firstBoundaryIndex === -1) return [contents];
83894
- const parts = [];
83895
- if (firstBoundaryIndex > 0) parts.push(contents.slice(0, firstBoundaryIndex));
83896
- let startIndex = firstBoundaryIndex;
83897
- for (;;) {
83898
- const nextBoundaryIndex = findLinePrefixIndex(contents, newlinePrefix, startIndex + 1);
83899
- if (nextBoundaryIndex === -1) break;
83900
- parts.push(contents.slice(startIndex, nextBoundaryIndex));
83901
- startIndex = nextBoundaryIndex;
83902
- }
83903
- parts.push(contents.slice(startIndex));
83904
- return parts;
83905
- }
83906
- function findLinePrefixIndex(contents, newlinePrefix, fromIndex) {
83907
- const index = contents.indexOf(newlinePrefix, fromIndex);
83908
- return index === -1 ? -1 : index + 1;
83909
- }
83910
- function maybeDetachOptionalString(value) {
83911
- return value == null ? value : detachString(value);
83912
- }
83913
- function parseRawLineType(firstChar) {
83914
- return firstChar === " " ? "context" : firstChar === "\\" ? "metadata" : firstChar === "+" ? "addition" : "deletion";
83915
- }
83916
- function getParsedLineContent(rawLine) {
83917
- const processedLine = rawLine.slice(1);
83918
- return detachString(processedLine === "" ? "\n" : processedLine);
83919
- }
83920
- function createContentGroup(type, deletionLineIndex, additionLineIndex) {
83921
- if (type === "change") return {
83922
- type: "change",
83923
- additions: 0,
83924
- deletions: 0,
83925
- additionLineIndex,
83926
- deletionLineIndex
83927
- };
84237
+ });
84238
+ const processDomainEventSafely = (event) => processDomainEvent(event).pipe(Effect.catchCause((cause) => {
84239
+ if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause);
84240
+ return Effect.logWarning("provider command reactor failed to process event", {
84241
+ eventType: event.type,
84242
+ cause: Cause.pretty(cause)
84243
+ });
84244
+ }));
84245
+ const worker = yield* makeDrainableWorker(processDomainEventSafely);
83928
84246
  return {
83929
- type: "context",
83930
- lines: 0,
83931
- additionLineIndex,
83932
- deletionLineIndex
84247
+ start: Effect.fn("start")(function* () {
84248
+ const processEvent = Effect.fn("processEvent")(function* (event) {
84249
+ if (event.type === "thread.runtime-mode-set" || event.type === "thread.turn-start-requested" || event.type === "thread.turn-interrupt-requested" || event.type === "thread.approval-response-requested" || event.type === "thread.user-input-response-requested" || event.type === "thread.session-stop-requested") return yield* worker.enqueue(event);
84250
+ });
84251
+ yield* Effect.forkScoped(Stream.runForEach(orchestrationEngine.streamDomainEvents, processEvent));
84252
+ }),
84253
+ drain: worker.drain
83933
84254
  };
83934
- }
84255
+ });
84256
+ const ProviderCommandReactorLive = Layer.effect(ProviderCommandReactor, make$2);
83935
84257
  //#endregion
83936
84258
  //#region src/checkpointing/Diffs.ts
83937
84259
  function parseTurnDiffFilesFromUnifiedDiff(diff) {
@@ -83972,6 +84294,7 @@ const make$1 = Effect.gen(function* () {
83972
84294
  const receiptBus = yield* RuntimeReceiptBus;
83973
84295
  const workspaceEntries = yield* WorkspaceEntries;
83974
84296
  const vcsStatusBroadcaster = yield* VcsStatusBroadcaster;
84297
+ const threadActivities = yield* ProjectionThreadActivityRepository;
83975
84298
  const appendRevertFailureActivity = (input) => Effect.all({
83976
84299
  commandId: serverCommandId("checkpoint-revert-failure"),
83977
84300
  activityId: serverEventId
@@ -84060,13 +84383,21 @@ const make$1 = Effect.gen(function* () {
84060
84383
  checkpointRef: targetCheckpointRef
84061
84384
  });
84062
84385
  yield* workspaceEntries.refresh(input.cwd);
84386
+ const turnChangedPaths = yield* threadActivities.listByThreadId({ threadId: input.threadId }).pipe(Effect.map((activities) => {
84387
+ const changedPaths = /* @__PURE__ */ new Set();
84388
+ for (const activity of activities) {
84389
+ if (activity.tone !== "tool" || activity.turnId !== input.turnId) continue;
84390
+ for (const changedPath of collectActivityChangedFilePaths(activity.payload)) changedPaths.add(changedPath);
84391
+ }
84392
+ return normalizeChangedFilePaths(changedPaths, input.cwd);
84393
+ }), Effect.orElseSucceed(() => /* @__PURE__ */ new Set()));
84063
84394
  const files = yield* checkpointStore.diffCheckpoints({
84064
84395
  cwd: input.cwd,
84065
84396
  fromCheckpointRef,
84066
84397
  toCheckpointRef: targetCheckpointRef,
84067
84398
  fallbackFromToHead: false,
84068
84399
  ignoreWhitespace: false
84069
- }).pipe(Effect.map((diff) => parseTurnDiffFilesFromUnifiedDiff(diff).map((file) => ({
84400
+ }).pipe(Effect.map((diff) => parseTurnDiffFilesFromUnifiedDiff(diff).filter((file) => turnChangedPaths.size === 0 || turnChangedPaths.has(file.path)).map((file) => ({
84070
84401
  path: file.path,
84071
84402
  kind: "modified",
84072
84403
  additions: file.additions,