@p4code/cli 0.1.20 → 0.1.22

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 (88) hide show
  1. package/dist/bin.mjs +789 -73
  2. package/dist/client/assets/{DiffPanel-Di8WylWU.js → DiffPanel-Cdf69_RS.js} +3 -3
  3. package/dist/client/assets/{FilePreviewPanel-CSqqsbYX.js → FilePreviewPanel-CkKXCbcG.js} +5 -5
  4. package/dist/client/assets/PreviewPanel-BtqdQqtN.js +2 -0
  5. package/dist/client/assets/{angular-html-CPSNyy71.js → angular-html-BPHFZwgm.js} +2 -2
  6. package/dist/client/assets/{angular-ts-BgndqOdx.js → angular-ts-CXoIeuGh.js} +2 -2
  7. package/dist/client/assets/{apl-CFWK0bta.js → apl-C6oPUNZZ.js} +2 -2
  8. package/dist/client/assets/arrow-right-7YX6fLbL.js +2 -0
  9. package/dist/client/assets/{astro-D2uzatlp.js → astro-Bq_n5uKv.js} +2 -2
  10. package/dist/client/assets/{blade-DfrK_8z3.js → blade-DYe65h_C.js} +2 -2
  11. package/dist/client/assets/{c-DyORVYNJ.js → c-BsDji_bt.js} +2 -2
  12. package/dist/client/assets/{cobol-BUWePdBc.js → cobol-CoC31Rpg.js} +2 -2
  13. package/dist/client/assets/{coffee-Ba8BYHli.js → coffee-DU1_O9cH.js} +2 -2
  14. package/dist/client/assets/{cpp-BX0iAGNA.js → cpp-D5_eYHZr.js} +2 -2
  15. package/dist/client/assets/{crystal-BDSdceoQ.js → crystal-3iIyvfhL.js} +2 -2
  16. package/dist/client/assets/{css-TKd0Sp86.js → css-B4bpFXEv.js} +2 -2
  17. package/dist/client/assets/{edge-Bus7Aab8.js → edge--YoYp8s_.js} +2 -2
  18. package/dist/client/assets/{elixir-BlT2g4EX.js → elixir-CZ1wqlQn.js} +2 -2
  19. package/dist/client/assets/{elm-DTGJZ36e.js → elm-DKdyYY92.js} +2 -2
  20. package/dist/client/assets/{erb-BF4yZGxQ.js → erb-PeUqy_9N.js} +2 -2
  21. package/dist/client/assets/{fileCommentAnnotations-C5BlQ5s_.js → fileCommentAnnotations-BTb-0nj3.js} +2 -2
  22. package/dist/client/assets/{git-rebase-GAe0_ZI9.js → git-rebase-1Z5OxWid.js} +2 -2
  23. package/dist/client/assets/{glimmer-js-GWmPSTNt.js → glimmer-js-Br0y4u5b.js} +2 -2
  24. package/dist/client/assets/{glimmer-ts-CDLqKTZ9.js → glimmer-ts-Oh0AMNJv.js} +2 -2
  25. package/dist/client/assets/{glsl-Bv3E3r0O.js → glsl-DI6kM2gF.js} +2 -2
  26. package/dist/client/assets/{graphql-B5-LupiK.js → graphql-VCSJCQkl.js} +2 -2
  27. package/dist/client/assets/{hack-KTQ3DPKm.js → hack-4xJQMtbZ.js} +2 -2
  28. package/dist/client/assets/{haml-BvC-52XY.js → haml-Bc0kPVkr.js} +2 -2
  29. package/dist/client/assets/{handlebars-CK9WCq20.js → handlebars-Znb8TzWA.js} +2 -2
  30. package/dist/client/assets/{html-BGjxg0hO.js → html-_Zw71zQT.js} +2 -2
  31. package/dist/client/assets/{html-derivative-C0EaHUuc.js → html-derivative-D6Fn1mLE.js} +2 -2
  32. package/dist/client/assets/{http-hvxfwUee.js → http-CSdnm1Wh.js} +2 -2
  33. package/dist/client/assets/{hurl-D5cuiBPB.js → hurl-DKOuNoWT.js} +2 -2
  34. package/dist/client/assets/index-CeztlSXj.js +853 -0
  35. package/dist/client/assets/index-eSuLivhH.css +1 -0
  36. package/dist/client/assets/{java-DOkofowB.js → java-Czo4Dn1f.js} +2 -2
  37. package/dist/client/assets/{javascript-dCesbfJ7.js → javascript-fyReKHSS.js} +2 -2
  38. package/dist/client/assets/{jinja-BDrVqT_2.js → jinja-CvJhR5Z5.js} +2 -2
  39. package/dist/client/assets/{jison-Dvzh0XRq.js → jison-DSlj1sEU.js} +2 -2
  40. package/dist/client/assets/{json-CpV_dVb2.js → json-fCB7n2pX.js} +2 -2
  41. package/dist/client/assets/{jsx-CbaKC498.js → jsx-C1OJlXdb.js} +2 -2
  42. package/dist/client/assets/{julia-BKYdI_5W.js → julia-CmLSMgqX.js} +2 -2
  43. package/dist/client/assets/{just-FrVMEGvg.js → just-D9KdTXpD.js} +2 -2
  44. package/dist/client/assets/{latex-BSmwt_7j.js → latex-BZoE-Z3X.js} +2 -2
  45. package/dist/client/assets/{liquid-Bb2nFp-U.js → liquid-DW5UrT3Q.js} +2 -2
  46. package/dist/client/assets/{lua-CEANnQ2c.js → lua-DMAjig-8.js} +2 -2
  47. package/dist/client/assets/{marko-CjJZ1eHg.js → marko-EN2dMS-t.js} +2 -2
  48. package/dist/client/assets/{mdc-B3VSAYVT.js → mdc-DV5R0Zdp.js} +2 -2
  49. package/dist/client/assets/{nginx-BkYJUm4E.js → nginx-BhF2pPjp.js} +2 -2
  50. package/dist/client/assets/{nim-D8xT6_xt.js → nim-Ck0oR8VY.js} +2 -2
  51. package/dist/client/assets/{perl-cazo7YHC.js → perl-B4V1RSOt.js} +2 -2
  52. package/dist/client/assets/{php-C0LwuOo3.js → php-BvwSzqRH.js} +2 -2
  53. package/dist/client/assets/{pug-OP6SwNFw.js → pug-DB3guzOM.js} +2 -2
  54. package/dist/client/assets/{qml-C1-WQIQ-.js → qml-CohVRZGq.js} +2 -2
  55. package/dist/client/assets/{r-PkhwsIgx.js → r-Doi-hRR2.js} +2 -2
  56. package/dist/client/assets/{razor-WONkd3aR.js → razor-CtBV-RZa.js} +2 -2
  57. package/dist/client/assets/{regexp-SDnpa4md.js → regexp-DgF6jh6T.js} +2 -2
  58. package/dist/client/assets/{rst-DulobrTh.js → rst-1GlNPFb8.js} +2 -2
  59. package/dist/client/assets/{ruby-RYHm8H3x.js → ruby-C_Jg-W0j.js} +2 -2
  60. package/dist/client/assets/{sas-BBwb67VZ.js → sas-IRPqQRQC.js} +2 -2
  61. package/dist/client/assets/{scss-y2IZtjKJ.js → scss-BPKDbfld.js} +2 -2
  62. package/dist/client/assets/{shellscript-B3vxtpJZ.js → shellscript-D5KbVhsx.js} +2 -2
  63. package/dist/client/assets/{shellsession-CvHfy0TS.js → shellsession-bEniUyPe.js} +2 -2
  64. package/dist/client/assets/{soy-DWpn3a9x.js → soy-D0Pcy9LW.js} +2 -2
  65. package/dist/client/assets/{sql-CCfTsIvj.js → sql-DF2TL966.js} +2 -2
  66. package/dist/client/assets/{stata-Dr5CN-Tp.js → stata-BLI2BTCU.js} +2 -2
  67. package/dist/client/assets/{surrealql-DgzXJo35.js → surrealql-BjtO5tmi.js} +2 -2
  68. package/dist/client/assets/{svelte-JQb5PCjP.js → svelte-IyvKNOcX.js} +2 -2
  69. package/dist/client/assets/{templ-DQt5AsQ7.js → templ-qRJlnfHP.js} +2 -2
  70. package/dist/client/assets/{tex-B8Qokwwv.js → tex-CgYv5ALc.js} +2 -2
  71. package/dist/client/assets/textarea-B03Qe3NX.js +46 -0
  72. package/dist/client/assets/{ts-tags-B-RrSBzw.js → ts-tags-YasuuPCT.js} +2 -2
  73. package/dist/client/assets/{tsx-C7Q0nn_B.js → tsx-BF7eypTR.js} +2 -2
  74. package/dist/client/assets/{twig-CzVGXsz7.js → twig-BF-FIMTp.js} +2 -2
  75. package/dist/client/assets/{typescript-irwEvPgw.js → typescript-CtOBCMI7.js} +2 -2
  76. package/dist/client/assets/{vue-BE-nMa-3.js → vue-BTbCYZab.js} +2 -2
  77. package/dist/client/assets/{vue-html-SXHQOmAo.js → vue-html-1KQNz6IV.js} +2 -2
  78. package/dist/client/assets/{vue-vine-D62pnm9I.js → vue-vine-BVHOJ5oI.js} +2 -2
  79. package/dist/client/assets/{xml-BnssPo1i.js → xml-CftlzqvG.js} +2 -2
  80. package/dist/client/assets/{xsl-Dg7cN1LO.js → xsl-DoQ9dHRM.js} +2 -2
  81. package/dist/client/assets/{yaml-BAeQKrXU.js → yaml-DMUX1MAF.js} +2 -2
  82. package/dist/client/index.html +3 -3
  83. package/package.json +1 -1
  84. package/dist/client/assets/PreviewPanel-BcRUQSMy.js +0 -2
  85. package/dist/client/assets/arrow-right-wbRntSmq.js +0 -2
  86. package/dist/client/assets/index-Dol3ssFp.css +0 -1
  87. package/dist/client/assets/index-QC2FhlVn.js +0 -836
  88. package/dist/client/assets/textarea-BlxAoGJe.js +0 -46
package/dist/bin.mjs CHANGED
@@ -233,7 +233,7 @@ const make$76 = () => {
233
233
  const layer$72 = Layer.sync(NetService, make$76);
234
234
  //#endregion
235
235
  //#region package.json
236
- var version = "0.1.20";
236
+ var version = "0.1.22";
237
237
  //#endregion
238
238
  //#region src/config.ts
239
239
  /**
@@ -7858,11 +7858,21 @@ var ProjectWriteFileError = class extends Schema$1.TaggedErrorClass()("ProjectWr
7858
7858
  * is a URL as often as an id, and which tracker a bare `MOBILE-12262` belongs
7859
7859
  * to is a question only the resolver can answer.
7860
7860
  */
7861
- const TicketResolveInput = Schema$1.Struct({ reference: TrimmedNonEmptyString });
7861
+ const TicketResolveInput = Schema$1.Struct({ reference: TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "The ticket as it was written: an identifier like MOBILE-12241, or a tracker URL. Pass it through verbatim rather than reformatting it." })) });
7862
7862
  const TicketResolveResult = Schema$1.Struct({
7863
7863
  externalRef: TaskExternalRef,
7864
7864
  /** The issue title as the tracker reports it, for the task that mirrors it. */
7865
- title: TrimmedNonEmptyString
7865
+ title: TrimmedNonEmptyString,
7866
+ /**
7867
+ * The issue body, when the tracker reports one.
7868
+ *
7869
+ * Optional because it is the one field here that has a legitimate empty
7870
+ * value - plenty of tickets are a title and nothing else - and because the
7871
+ * composer, which only ever renders a chip, does not read it. It is carried
7872
+ * for the caller that does: an agent handed a ticket id needs the
7873
+ * requirements, and the title alone sends it back to ask for them.
7874
+ */
7875
+ description: Schema$1.optional(TrimmedNonEmptyString)
7866
7876
  });
7867
7877
  /**
7868
7878
  * Why a reference could not be turned into a ticket.
@@ -7889,18 +7899,35 @@ var TicketResolveError = class extends Schema$1.TaggedErrorClass()("TicketResolv
7889
7899
  //#endregion
7890
7900
  //#region ../../packages/contracts/src/threadControl.ts
7891
7901
  /**
7892
- * Thread control over MCP - the self-scoped toolkit an agent uses to act on
7893
- * the thread it is running in.
7894
- *
7895
- * Deliberately narrow. The reported need was an agent renaming its own thread;
7896
- * that needs no cross-thread reach, and the per-session bearer token is the
7897
- * only thing guarding `/mcp` on a remote-reachable server, so a capability
7898
- * that could rename arbitrary threads would be a capability to vandalise the
7899
- * workspace from one leaked token. Delete and create are withheld outright,
7900
- * the same answer the task toolkit gave. Settle is absent for a structural
7901
- * reason rather than a policy one: the decider refuses `thread.settle` while
7902
- * the thread's session is starting or running, and a self-scoped MCP call
7903
- * always arrives from its own live session, so the call could never succeed.
7902
+ * Thread control over MCP - the toolkit an agent uses to act on its own thread
7903
+ * and on the threads it started.
7904
+ *
7905
+ * **The scope rule.** A per-session bearer token is the only thing guarding
7906
+ * `/mcp` on a remote-reachable server, so reach is granted, never assumed. A
7907
+ * session may address its own thread and the threads it created through
7908
+ * `thread_spawn`, and nothing else. A leaked token therefore still reaches only
7909
+ * work that token itself began. The grant is held on the credential rather than
7910
+ * derived from the board, so it cannot widen because someone edited a task.
7911
+ *
7912
+ * **Delete is not exposed, and that is the decision rather than an omission.**
7913
+ * It is the one irreversible action here and a thread carries its whole
7914
+ * transcript. The task toolkit gave the same answer for the same reason, and a
7915
+ * thread is the more expensive thing to lose. An agent that wants a thread gone
7916
+ * settles it and says so; a person deletes it.
7917
+ *
7918
+ * **Spawning is one level deep.** `thread_spawn` seeds a first message and
7919
+ * starts the turn, because a create that cannot say anything is not the feature
7920
+ * anyone asked for. A thread that was itself spawned may not spawn: the depth
7921
+ * cap is what a runaway loop runs into, and one level is enough for "split this
7922
+ * task and start the pieces" while still being a structural bound rather than a
7923
+ * prompt asking nicely.
7924
+ *
7925
+ * **Settle is queued rather than refused.** The decider refuses `thread.settle`
7926
+ * while the target's session is starting or running, and an MCP call from a
7927
+ * thread's own session always arrives while that session is alive - so the
7928
+ * self-addressed case could never succeed as an immediate command. It is
7929
+ * recorded instead and applied when the session stops, which is what an agent
7930
+ * settling its own thread means anyway: finish, then close.
7904
7931
  *
7905
7932
  * Quick-append to memory lives here too: it is the other thing an agent wants
7906
7933
  * to do *from* a thread, and it shares the toolkit's capability and scope.
@@ -7918,6 +7945,64 @@ const ThreadRenameResult = Schema$1.Struct({
7918
7945
  title: TrimmedNonEmptyString
7919
7946
  });
7920
7947
  /**
7948
+ * Which thread a control tool acts on.
7949
+ *
7950
+ * Optional, and absent means the caller's own thread. That default is what
7951
+ * keeps the common case honest: an agent adjusting itself never has to name an
7952
+ * id, so it cannot name the wrong one, and every tool below reads the same way
7953
+ * whether or not it is addressed elsewhere.
7954
+ */
7955
+ const ControlTargetThreadId = Schema$1.optional(ThreadId.annotate({ description: "The thread to act on. Omit for this session's own thread. Only threads this session started with thread_spawn can be named here; any other id is refused." }));
7956
+ const ThreadSpawnInput = Schema$1.Struct({
7957
+ title: TrimmedNonEmptyString.annotate({ description: "Title for the new thread. Shown on the board and in the sidebar." }),
7958
+ prompt: TrimmedNonEmptyString.annotate({ description: "The first message to send. The thread starts its turn immediately, so this is the whole brief the new agent gets." }),
7959
+ projectId: Schema$1.optional(ProjectId.annotate({ description: "Project to start the thread in. Defaults to this session's own project." })),
7960
+ runtimeMode: Schema$1.optional(RuntimeMode.annotate({ description: "Permission mode for the new thread. Defaults to this session's own." })),
7961
+ interactionMode: Schema$1.optional(ProviderInteractionMode),
7962
+ compressMode: Schema$1.optional(CompressMode),
7963
+ unpromptedSubagents: Schema$1.optional(Schema$1.Boolean)
7964
+ });
7965
+ const ThreadSpawnResult = Schema$1.Struct({
7966
+ /** Watchable with `thread_watch_events`, and addressable by every tool here. */
7967
+ threadId: ThreadId,
7968
+ projectId: ProjectId,
7969
+ title: TrimmedNonEmptyString
7970
+ });
7971
+ const ThreadConfigureInput = Schema$1.Struct({
7972
+ threadId: ControlTargetThreadId,
7973
+ title: Schema$1.optional(TrimmedNonEmptyString),
7974
+ modelSelection: Schema$1.optional(ModelSelection),
7975
+ runtimeMode: Schema$1.optional(RuntimeMode),
7976
+ interactionMode: Schema$1.optional(ProviderInteractionMode),
7977
+ compressMode: Schema$1.optional(CompressMode),
7978
+ unpromptedSubagents: Schema$1.optional(Schema$1.Boolean)
7979
+ });
7980
+ const ThreadConfigureResult = Schema$1.Struct({
7981
+ threadId: ThreadId,
7982
+ /** The settings that were actually changed, in the order they were applied. */
7983
+ applied: Schema$1.Array(Schema$1.String)
7984
+ });
7985
+ const ThreadSettleInput = Schema$1.Struct({ threadId: ControlTargetThreadId });
7986
+ /**
7987
+ * Settled now, or recorded to settle when the session stops.
7988
+ *
7989
+ * Two outcomes rather than one because the difference is the agent's to know: a
7990
+ * queued settle has not happened yet, and a thread that goes on to do more work
7991
+ * still settles at the end of it.
7992
+ */
7993
+ const ThreadSettleResult = Schema$1.Struct({
7994
+ threadId: ThreadId,
7995
+ outcome: Schema$1.Literals(["settled", "queued"])
7996
+ });
7997
+ const ThreadSnoozeInput = Schema$1.Struct({
7998
+ threadId: ControlTargetThreadId,
7999
+ snoozedUntil: IsoDateTime.annotate({ description: "When the thread should come back, as an ISO 8601 timestamp." })
8000
+ });
8001
+ const ThreadSnoozeResult = Schema$1.Struct({
8002
+ threadId: ThreadId,
8003
+ snoozedUntil: IsoDateTime
8004
+ });
8005
+ /**
7921
8006
  * Which memory file an append lands in.
7922
8007
  *
7923
8008
  * A closed union rather than a filename, because the target directory also
@@ -8012,8 +8097,42 @@ var AssetCompressValidationFailedError = class extends Schema$1.TaggedErrorClass
8012
8097
  return `The compressed ${this.path} did not validate, so nothing was written: ${this.errors.join(" ")}`;
8013
8098
  }
8014
8099
  };
8100
+ /**
8101
+ * The named thread is outside this session's grant.
8102
+ *
8103
+ * Deliberately the same answer whether the thread does not exist or exists and
8104
+ * belongs to someone else: a caller that could tell those apart could map the
8105
+ * whole board one id at a time.
8106
+ */
8107
+ var ThreadControlNotPermittedError = class extends Schema$1.TaggedErrorClass()("ThreadControlNotPermittedError", { threadId: ThreadId }) {
8108
+ get message() {
8109
+ return `This session cannot act on thread ${this.threadId}. A session may act on its own thread and on threads it started with thread_spawn.`;
8110
+ }
8111
+ };
8112
+ /** A spawned thread asked to spawn. One level is the whole allowance. */
8113
+ var ThreadSpawnNotPermittedError = class extends Schema$1.TaggedErrorClass()("ThreadSpawnNotPermittedError", {
8114
+ threadId: ThreadId,
8115
+ detail: Schema$1.String
8116
+ }) {
8117
+ get message() {
8118
+ return `Thread ${this.threadId} cannot start another thread: ${this.detail}`;
8119
+ }
8120
+ };
8121
+ /** The orchestration engine declined or failed a thread control command. */
8122
+ var ThreadControlRejectedError = class extends Schema$1.TaggedErrorClass()("ThreadControlRejectedError", {
8123
+ threadId: ThreadId,
8124
+ commandType: Schema$1.String,
8125
+ detail: Schema$1.String
8126
+ }) {
8127
+ get message() {
8128
+ return `${this.commandType} on thread ${this.threadId} failed: ${this.detail}`;
8129
+ }
8130
+ };
8015
8131
  const ThreadControlToolError = Schema$1.Union([
8016
8132
  ThreadToolUnavailableError,
8133
+ ThreadControlNotPermittedError,
8134
+ ThreadSpawnNotPermittedError,
8135
+ ThreadControlRejectedError,
8017
8136
  ThreadRenameRejectedError,
8018
8137
  MemoryAppendFailedError,
8019
8138
  AssetCompressRefusedError,
@@ -14929,6 +15048,7 @@ const makeTaskRepository = Effect.gen(function* () {
14929
15048
  ...input.patch.projectId !== void 0 ? { projectId: input.patch.projectId } : {},
14930
15049
  ...input.patch.repositoryKey !== void 0 ? { repositoryKey: input.patch.repositoryKey } : {},
14931
15050
  ...input.patch.threadId !== void 0 ? { threadId: input.patch.threadId } : {},
15051
+ ...input.patch.externalRef !== void 0 ? { externalRef: input.patch.externalRef } : {},
14932
15052
  ...input.patch.parentTaskId !== void 0 ? { parentTaskId: input.patch.parentTaskId } : {},
14933
15053
  ...input.patch.title !== void 0 ? { title: input.patch.title } : {},
14934
15054
  ...input.patch.body !== void 0 ? { body: input.patch.body } : {},
@@ -18870,6 +18990,16 @@ function compressTurnReminderFor(mode) {
18870
18990
  */
18871
18991
  const PRESET_MAX_RESULT_ROWS = 20;
18872
18992
  /**
18993
+ * The tool calls a preset run is allowed.
18994
+ *
18995
+ * Fifteen because a located answer typically takes six to ten, and the runs
18996
+ * that overshoot do not overshoot slightly: measured against real transcripts,
18997
+ * the long tail sits between sixty and a hundred and thirty calls and carries
18998
+ * nearly all of the cost. A cap that a good run never reaches is a cap that only
18999
+ * ever bites the runs that had stopped making progress.
19000
+ */
19001
+ const PRESET_MAX_TOOL_CALLS = 15;
19002
+ /**
18873
19003
  * Named `p4-*` on purpose.
18874
19004
  *
18875
19005
  * The directory is shared with whatever the user has written by hand, and
@@ -18899,6 +19029,34 @@ names a path that is not there, answer with the BLOCKED line below. Guessing at
18899
19029
  the missing half of a request produces an answer that reads as confident and is
18900
19030
  not checkable, which is worse than no answer.`;
18901
19031
  /**
19032
+ * What every preset says about the run in between.
19033
+ *
19034
+ * Three rules, each aimed at a measured failure. The cap bounds the quadratic.
19035
+ * The ban on nesting stops a bounded run from spawning an unbounded one, which
19036
+ * is the same bill again with nothing watching it. The stop-at-the-cap clause is
19037
+ * what makes the number safe to enforce: without somewhere to put a partial
19038
+ * answer, a capped subagent either grinds past the cap or throws away what it
19039
+ * had, and both are worse than a short table with a named gap.
19040
+ */
19041
+ const WORK_BUDGET = `## What the run costs
19042
+
19043
+ At most ${PRESET_MAX_TOOL_CALLS} tool calls. Most answers of this kind take six
19044
+ to ten, so treat the cap as the point where something has gone wrong rather than
19045
+ as a target to spend.
19046
+
19047
+ Spend them on calls that can fail usefully: before each one, know what a hit and
19048
+ a miss would each tell you. Batch searches into one call separated by echoed
19049
+ markers rather than issuing them one at a time, bound every output with a head
19050
+ or a line count, and read a file only after a search has told you which lines
19051
+ matter, and read those lines. Never repeat a call you have already made.
19052
+
19053
+ Do not spawn a subagent of your own. You are the bounded run; one that starts
19054
+ another has spent the budget twice with nothing watching the second half.
19055
+
19056
+ Reaching the cap ends the run. Return the rows you have verified, and add a
19057
+ final line naming the search you would have run next. With no verified rows at
19058
+ all, that is the BLOCKED line.`;
19059
+ /**
18902
19060
  * What every preset says about the answer.
18903
19061
  *
18904
19062
  * The two sentinels matter as much as the table. Without `NONE` a clean run
@@ -18917,7 +19075,8 @@ const outputContract = (row, extra) => [
18917
19075
  ` ${row}`,
18918
19076
  "",
18919
19077
  "Nothing to report: the single word NONE.",
18920
- "Request insufficient: BLOCKED - <what is missing>, on one line.",
19078
+ "Request insufficient, or the budget spent before anything was verified:",
19079
+ "BLOCKED - <what is missing>, on one line.",
18921
19080
  "",
18922
19081
  "Paths are repository-relative and carry a line number. A row without one is",
18923
19082
  "a row you have not verified: open the file and get it.",
@@ -18929,6 +19088,11 @@ const outputContract = (row, extra) => [
18929
19088
  * The compression ruleset goes last so the contract above it is read first.
18930
19089
  * Both are instructions to the same model and neither overrides the other:
18931
19090
  * the contract fixes the shape, the ruleset fixes the prose inside it.
19091
+ *
19092
+ * `model` is optional, and omitting it inherits the session's. Naming a cheaper
19093
+ * tier is worth it only where the job is mechanical enough that the tier is not
19094
+ * what makes the answer right, which is true of locating a symbol and is not
19095
+ * true of deciding whether a line is a bug.
18932
19096
  */
18933
19097
  const buildPreset = (input) => {
18934
19098
  const name = `${PRESET_NAME_PREFIX}${input.name}`;
@@ -18937,6 +19101,7 @@ const buildPreset = (input) => {
18937
19101
  `name: ${name}`,
18938
19102
  `description: ${JSON.stringify(input.description)}`,
18939
19103
  `tools: ${input.tools}`,
19104
+ ...input.model === void 0 ? [] : [`model: ${input.model}`],
18940
19105
  "---",
18941
19106
  "",
18942
19107
  input.body.trim(),
@@ -18959,12 +19124,15 @@ const AGENT_PRESETS = [buildPreset({
18959
19124
  name: "investigator",
18960
19125
  description: "Find where something lives in the codebase. Use when the answer is a set of locations rather than a change. Returns a fixed table of path:line rows and nothing else.",
18961
19126
  tools: "Read, Grep, Glob, Bash",
19127
+ model: "sonnet",
18962
19128
  summary: "Locates code and answers with a path:line table.",
18963
19129
  body: `You locate code. You do not change it, and you say no more about any location
18964
19130
  than the one clause that explains why it is in the list.
18965
19131
 
18966
19132
  ${INPUT_CONTRACT}
18967
19133
 
19134
+ ${WORK_BUDGET}
19135
+
18968
19136
  ${outputContract("path:line - symbol - one clause on why it matters", [
18969
19137
  "",
18970
19138
  "Symbol is the function, type or constant the line belongs to, not a paraphrase",
@@ -18982,6 +19150,8 @@ approval is a list that has to be re-read to find the three rows that matter.
18982
19150
 
18983
19151
  ${INPUT_CONTRACT}
18984
19152
 
19153
+ ${WORK_BUDGET}
19154
+
18985
19155
  ${outputContract("path:line - severity - what is wrong, and what it breaks", [
18986
19156
  "",
18987
19157
  "Severity is one of three words:",
@@ -34704,6 +34874,7 @@ const TASK_PLAN_TOOL_NAMES = /* @__PURE__ */ new Set([
34704
34874
  "task_get",
34705
34875
  "task_current",
34706
34876
  "task_propose",
34877
+ "ticket_resolve",
34707
34878
  "taskcreate",
34708
34879
  "taskupdate",
34709
34880
  "tasklist",
@@ -36411,13 +36582,16 @@ const make$36 = Effect.gen(function* () {
36411
36582
  reason: "not_found",
36412
36583
  detail: `Linear returned nothing usable for ${identifier}.`
36413
36584
  });
36585
+ const externalRef = {
36586
+ source: "linear",
36587
+ identifier: stringField$1(issue, "identifier") ?? identifier,
36588
+ url
36589
+ };
36590
+ const description = stringField$1(issue, "description");
36414
36591
  return {
36415
- externalRef: {
36416
- source: "linear",
36417
- identifier: stringField$1(issue, "identifier") ?? identifier,
36418
- url
36419
- },
36420
- title
36592
+ externalRef,
36593
+ title,
36594
+ ...description === void 0 ? {} : { description }
36421
36595
  };
36422
36596
  }) };
36423
36597
  });
@@ -38572,6 +38746,38 @@ const requireThreadCapability = Effect.fn("mcp.requireThreadCapability")(functio
38572
38746
  });
38573
38747
  return invocation;
38574
38748
  });
38749
+ /**
38750
+ * The thread a control tool may act on, given what it was asked to act on.
38751
+ *
38752
+ * Two checks like `requireWatchCapability`, and for the same reason: the
38753
+ * capability says this session controls threads at all, the grant says which.
38754
+ * An omitted id resolves to the session's own thread, which is always in the
38755
+ * grant and is why the common call needs no id and so cannot name a wrong one.
38756
+ */
38757
+ const requireThreadControlTarget = Effect.fn("mcp.requireThreadControlTarget")(function* (requested) {
38758
+ const invocation = yield* requireThreadCapability();
38759
+ if (requested === void 0 || requested === invocation.threadId) return {
38760
+ invocation,
38761
+ threadId: invocation.threadId
38762
+ };
38763
+ if (!invocation.controlThreadIds?.has(requested)) return yield* new ThreadControlNotPermittedError({ threadId: requested });
38764
+ return {
38765
+ invocation,
38766
+ threadId: requested
38767
+ };
38768
+ });
38769
+ /**
38770
+ * The guard on starting a thread, which is a different question from acting on
38771
+ * one: it is not "which thread" but "may this session make more of them".
38772
+ */
38773
+ const requireThreadSpawn = Effect.fn("mcp.requireThreadSpawn")(function* () {
38774
+ const invocation = yield* requireThreadCapability();
38775
+ if (invocation.mayCreateThreads !== true) return yield* new ThreadSpawnNotPermittedError({
38776
+ threadId: invocation.threadId,
38777
+ detail: "this thread was itself started by an agent, and spawning goes one level deep so a runaway loop has a bound"
38778
+ });
38779
+ return invocation;
38780
+ });
38575
38781
  //#endregion
38576
38782
  //#region src/mcp/PreviewAutomationBroker.ts
38577
38783
  var PreviewAutomationBroker = class extends Context.Service()("@p4code/cli/mcp/PreviewAutomationBroker") {};
@@ -46688,6 +46894,7 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
46688
46894
  ...row.projectId === null ? {} : { projectId: row.projectId },
46689
46895
  ...row.repositoryKey === null ? {} : { repositoryKey: row.repositoryKey },
46690
46896
  ...row.threadId === null ? {} : { threadId: row.threadId },
46897
+ ...row.externalRef === null ? {} : { externalRef: row.externalRef },
46691
46898
  ...row.parentTaskId === null ? {} : { parentTaskId: row.parentTaskId },
46692
46899
  title: row.title,
46693
46900
  body: row.body,
@@ -47534,6 +47741,18 @@ const ProviderEventLoggersLive = Layer.effect(ProviderEventLoggers, Effect.gen(f
47534
47741
  var ProviderService = class extends Context.Service()("@p4code/cli/provider/Services/ProviderService") {};
47535
47742
  //#endregion
47536
47743
  //#region src/mcp/McpProviderSession.ts
47744
+ /**
47745
+ * The name p4code declares its own toolkit under, in every provider.
47746
+ *
47747
+ * One constant because it is one name in the agent's world: prompts, skills and
47748
+ * the tool categories all say `p4-code`, whichever runtime is reading them.
47749
+ * Every adapter also excludes it from the user's registered servers, so a
47750
+ * registration of the same name cannot displace the built-in one and take the
47751
+ * per-thread credential's tools (`task_current`, `thread_*`) with it.
47752
+ */
47753
+ const P4CODE_MCP_SERVER_NAME = "p4-code";
47754
+ /** The reserved names, as the set the mappers take. */
47755
+ const P4CODE_MCP_SERVER_NAMES = /* @__PURE__ */ new Set([P4CODE_MCP_SERVER_NAME]);
47537
47756
  const sessionsByThread = /* @__PURE__ */ new Map();
47538
47757
  function setMcpProviderSession(config) {
47539
47758
  sessionsByThread.set(config.threadId, config);
@@ -47576,7 +47795,10 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
47576
47795
  const crypto = yield* Crypto.Crypto;
47577
47796
  const environmentId = yield* (yield* ServerEnvironment).getEnvironmentId;
47578
47797
  const httpServer = yield* HttpServer.HttpServer;
47579
- const state = yield* SynchronizedRef.make({ records: /* @__PURE__ */ new Map() });
47798
+ const state = yield* SynchronizedRef.make({
47799
+ records: /* @__PURE__ */ new Map(),
47800
+ spawnedThreadIds: /* @__PURE__ */ new Set()
47801
+ });
47580
47802
  const currentTimeMillis = options.now ? Effect.sync(options.now) : Clock.currentTimeMillis;
47581
47803
  const livenessWindowMs = options.livenessWindowMs ?? DEFAULT_LIVENESS_WINDOW_MS;
47582
47804
  const endpoint = httpServer.address._tag === "TcpAddress" ? `http://${getHttpMcpEndpointHost(httpServer.address.hostname)}:${httpServer.address.port}/mcp` : "http://127.0.0.1/mcp";
@@ -47597,29 +47819,35 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
47597
47819
  "threads"
47598
47820
  ]);
47599
47821
  if (watchThreadIds.size > 0) capabilities.add("watch");
47600
- const scope = {
47822
+ const threadId = ThreadId.make(request.threadId);
47823
+ const scopeWith = (mayCreateThreads) => ({
47601
47824
  environmentId,
47602
- threadId: ThreadId.make(request.threadId),
47825
+ threadId,
47603
47826
  providerSessionId,
47604
47827
  providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),
47605
47828
  capabilities,
47606
47829
  ...watchThreadIds.size > 0 ? { watchThreadIds } : {},
47830
+ mayCreateThreads,
47607
47831
  issuedAt
47608
- };
47609
- yield* SynchronizedRef.update(state, ({ records }) => {
47832
+ });
47833
+ yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
47610
47834
  const next = new Map(pruneDead(records, issuedAt));
47611
47835
  next.set(tokenHash, {
47612
47836
  tokenHash,
47613
- scope,
47837
+ scope: scopeWith(!spawnedThreadIds.has(threadId)),
47838
+ controlThreadIds: /* @__PURE__ */ new Set(),
47614
47839
  lastAliveAt: issuedAt
47615
47840
  });
47616
- return { records: next };
47841
+ return {
47842
+ records: next,
47843
+ spawnedThreadIds
47844
+ };
47617
47845
  });
47618
47846
  return { config: {
47619
47847
  environmentId,
47620
- threadId: scope.threadId,
47848
+ threadId,
47621
47849
  providerSessionId,
47622
- providerInstanceId: scope.providerInstanceId,
47850
+ providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),
47623
47851
  endpoint,
47624
47852
  authorizationHeader: `Bearer ${rawToken}`
47625
47853
  } };
@@ -47628,42 +47856,76 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
47628
47856
  if (rawToken.length === 0) return void 0;
47629
47857
  const tokenHash = yield* hashToken(rawToken);
47630
47858
  const timestamp = yield* currentTimeMillis;
47631
- return yield* SynchronizedRef.modify(state, ({ records }) => {
47859
+ return yield* SynchronizedRef.modify(state, ({ records, spawnedThreadIds }) => {
47632
47860
  const current = pruneDead(records, timestamp);
47633
47861
  const record = current.get(tokenHash);
47634
- if (!record) return [void 0, { records: current }];
47862
+ if (!record) return [void 0, {
47863
+ records: current,
47864
+ spawnedThreadIds
47865
+ }];
47635
47866
  const next = new Map(current);
47636
47867
  next.set(tokenHash, {
47637
47868
  ...record,
47638
47869
  lastAliveAt: timestamp
47639
47870
  });
47640
- return [record.scope, { records: next }];
47871
+ return [record.controlThreadIds.size === 0 ? record.scope : {
47872
+ ...record.scope,
47873
+ capabilities: /* @__PURE__ */ new Set([...record.scope.capabilities, "watch"]),
47874
+ controlThreadIds: record.controlThreadIds,
47875
+ watchThreadIds: /* @__PURE__ */ new Set([...record.scope.watchThreadIds ?? [], ...record.controlThreadIds])
47876
+ }, {
47877
+ records: next,
47878
+ spawnedThreadIds
47879
+ }];
47641
47880
  });
47642
47881
  });
47643
47882
  const touch = Effect.fn("McpSessionRegistry.touch")(function* (threadId) {
47644
47883
  const timestamp = yield* currentTimeMillis;
47645
- yield* SynchronizedRef.update(state, ({ records }) => {
47884
+ yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
47646
47885
  const current = pruneDead(records, timestamp);
47647
47886
  const next = new Map(current);
47648
47887
  for (const [tokenHash, record] of current) if (record.scope.threadId === threadId) next.set(tokenHash, {
47649
47888
  ...record,
47650
47889
  lastAliveAt: timestamp
47651
47890
  });
47652
- return { records: next };
47891
+ return {
47892
+ records: next,
47893
+ spawnedThreadIds
47894
+ };
47895
+ });
47896
+ });
47897
+ const recordSpawnedThread = Effect.fn("McpSessionRegistry.recordSpawnedThread")(function* (input) {
47898
+ yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
47899
+ const next = new Map(records);
47900
+ for (const [tokenHash, record] of records) if (record.scope.providerSessionId === input.providerSessionId) next.set(tokenHash, {
47901
+ ...record,
47902
+ controlThreadIds: /* @__PURE__ */ new Set([...record.controlThreadIds, input.threadId])
47903
+ });
47904
+ return {
47905
+ records: next,
47906
+ spawnedThreadIds: /* @__PURE__ */ new Set([...spawnedThreadIds, input.threadId])
47907
+ };
47653
47908
  });
47654
47909
  });
47655
- const revokeWhere = (predicate) => SynchronizedRef.update(state, ({ records }) => ({ records: new Map(Array.from(records).filter(([, record]) => !predicate(record))) }));
47910
+ const revokeWhere = (predicate) => SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => ({
47911
+ records: new Map(Array.from(records).filter(([, record]) => !predicate(record))),
47912
+ spawnedThreadIds
47913
+ }));
47656
47914
  return McpSessionRegistry.of({
47657
47915
  issue,
47658
47916
  resolve,
47659
47917
  touch,
47918
+ recordSpawnedThread,
47660
47919
  revokeProviderSession: Effect.fn("McpSessionRegistry.revokeProviderSession")(function* (providerSessionId) {
47661
47920
  yield* revokeWhere((record) => record.scope.providerSessionId === providerSessionId);
47662
47921
  }),
47663
47922
  revokeThread: Effect.fn("McpSessionRegistry.revokeThread")(function* (threadId) {
47664
47923
  yield* revokeWhere((record) => record.scope.threadId === threadId);
47665
47924
  }),
47666
- revokeAll: SynchronizedRef.set(state, { records: /* @__PURE__ */ new Map() })
47925
+ revokeAll: SynchronizedRef.update(state, ({ spawnedThreadIds }) => ({
47926
+ records: /* @__PURE__ */ new Map(),
47927
+ spawnedThreadIds
47928
+ }))
47667
47929
  });
47668
47930
  });
47669
47931
  let activeMcpSessionRegistry;
@@ -71327,6 +71589,81 @@ const SERVER_NOTIFICATION_PARAMS = {
71327
71589
  "account/login/completed": V2AccountLoginCompletedNotification
71328
71590
  };
71329
71591
  //#endregion
71592
+ //#region src/provider/Layers/codexMcpArgs.ts
71593
+ /** Codex's own name for the token variable of p4code's built-in server. */
71594
+ const CODEX_P4CODE_BEARER_TOKEN_ENV_VAR = "P4_MCP_BEARER_TOKEN";
71595
+ /** Prefix for the per-server variables the registered servers get. */
71596
+ const BEARER_TOKEN_ENV_VAR_PREFIX = "P4_MCP_BEARER_TOKEN_";
71597
+ /** TOML bare keys; anything else has to be quoted inside the dotted path. */
71598
+ const BARE_KEY = /^[A-Za-z0-9_-]+$/;
71599
+ const AUTHORIZATION_HEADER = "authorization";
71600
+ const BEARER_PREFIX = /^Bearer\s+/;
71601
+ const configKey = (name, field) => `mcp_servers.${BARE_KEY.test(name) ? name : JSON.stringify(name)}.${field}`;
71602
+ /**
71603
+ * A variable name derived from the server's, uniquely.
71604
+ *
71605
+ * Uppercased with every character a shell would not accept replaced, which can
71606
+ * collide (`a.b` and `a-b`), so the sanitized name is the prefix and the index
71607
+ * keeps it unique. The index is stable within one call, which is all that is
71608
+ * needed: the variable and the reference to it are emitted together.
71609
+ */
71610
+ const bearerTokenEnvVar = (name, index) => `${BEARER_TOKEN_ENV_VAR_PREFIX}${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_${index}`;
71611
+ /**
71612
+ * @param resolved - Servers keyed by name, as the registry resolved them.
71613
+ * @param exclude - Names p4code declares itself. A registration cannot displace
71614
+ * the built-in `p4-code` server, whose per-thread credential is the only
71615
+ * thing that makes `task_current` and the thread tools work.
71616
+ */
71617
+ const toCodexMcpConfig = (resolved, exclude = /* @__PURE__ */ new Set()) => {
71618
+ const args = [];
71619
+ const env = {};
71620
+ const skipped = [];
71621
+ const push = (name, field, value) => {
71622
+ args.push("-c", `${configKey(name, field)}=${value}`);
71623
+ };
71624
+ let index = 0;
71625
+ for (const [name, server] of Object.entries(resolved)) {
71626
+ if (exclude.has(name)) continue;
71627
+ if (server.type === "stdio") {
71628
+ push(name, "command", JSON.stringify(server.command));
71629
+ push(name, "args", JSON.stringify(server.args));
71630
+ if (Object.keys(server.env).length > 0) push(name, "env", `{ ${Object.entries(server.env).map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`).join(", ")} }`);
71631
+ index += 1;
71632
+ continue;
71633
+ }
71634
+ if (server.type === "sse") {
71635
+ skipped.push({
71636
+ name,
71637
+ reason: "Codex speaks streamable HTTP, not SSE."
71638
+ });
71639
+ continue;
71640
+ }
71641
+ const headerNames = Object.keys(server.headers);
71642
+ const authorization = headerNames.find((key) => key.toLowerCase() === AUTHORIZATION_HEADER);
71643
+ const otherHeaders = headerNames.filter((key) => key !== authorization);
71644
+ if (otherHeaders.length > 0) {
71645
+ skipped.push({
71646
+ name,
71647
+ reason: `Codex sends no custom headers, and this server needs ${otherHeaders.join(", ")}.`
71648
+ });
71649
+ continue;
71650
+ }
71651
+ push(name, "url", JSON.stringify(server.url));
71652
+ if (authorization !== void 0) {
71653
+ const token = server.headers[authorization]?.replace(BEARER_PREFIX, "") ?? "";
71654
+ const variable = bearerTokenEnvVar(name, index);
71655
+ env[variable] = token;
71656
+ push(name, "bearer_token_env_var", JSON.stringify(variable));
71657
+ }
71658
+ index += 1;
71659
+ }
71660
+ return {
71661
+ args,
71662
+ env,
71663
+ skipped
71664
+ };
71665
+ };
71666
+ //#endregion
71330
71667
  //#region ../../packages/effect-codex-app-server/src/_internal/shared.ts
71331
71668
  const JsonRpcId$1 = Schema$1.Union([Schema$1.Number, Schema$1.String]);
71332
71669
  const JsonRpcError = Schema$1.Struct({
@@ -73723,6 +74060,11 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (codexConfig, o
73723
74060
  if (existing && !existing.stopped) yield* Effect.suspend(() => stopSessionInternal(existing));
73724
74061
  const serviceTier = input.modelSelection?.instanceId === boundInstanceId ? getCodexServiceTierOptionValue(input.modelSelection) : void 0;
73725
74062
  const mcpSession = readMcpProviderSession(input.threadId);
74063
+ const externalMcp = toCodexMcpConfig(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, P4CODE_MCP_SERVER_NAMES);
74064
+ for (const dropped of externalMcp.skipped) yield* Effect.logWarning("codex cannot express an mcp registration", {
74065
+ server: dropped.name,
74066
+ reason: dropped.reason
74067
+ });
73726
74068
  const runtimeInput = {
73727
74069
  threadId: input.threadId,
73728
74070
  providerInstanceId: boundInstanceId,
@@ -73735,17 +74077,18 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (codexConfig, o
73735
74077
  runtimeMode: input.runtimeMode,
73736
74078
  ...input.modelSelection?.instanceId === boundInstanceId ? { model: input.modelSelection.model } : {},
73737
74079
  ...serviceTier ? { serviceTier } : {},
73738
- ...mcpSession ? {
74080
+ ...externalMcp.args.length > 0 || mcpSession ? {
73739
74081
  environment: {
73740
74082
  ...options?.environment ?? process.env,
73741
- P4_MCP_BEARER_TOKEN: mcpSession.authorizationHeader.replace(/^Bearer\s+/, "")
74083
+ ...externalMcp.env,
74084
+ ...mcpSession ? { [CODEX_P4CODE_BEARER_TOKEN_ENV_VAR]: mcpSession.authorizationHeader.replace(/^Bearer\s+/, "") } : {}
73742
74085
  },
73743
- appServerArgs: [
74086
+ appServerArgs: [...externalMcp.args, ...mcpSession ? [
73744
74087
  "-c",
73745
- `mcp_servers.p4-code.url=${mcpSession.endpoint}`,
74088
+ `mcp_servers.${P4CODE_MCP_SERVER_NAME}.url=${mcpSession.endpoint}`,
73746
74089
  "-c",
73747
- "mcp_servers.p4-code.bearer_token_env_var=\"P4_MCP_BEARER_TOKEN\""
73748
- ]
74090
+ `mcp_servers.${P4CODE_MCP_SERVER_NAME}.bearer_token_env_var="${CODEX_P4CODE_BEARER_TOKEN_ENV_VAR}"`
74091
+ ] : []]
73749
74092
  } : {}
73750
74093
  };
73751
74094
  const sessionScope = yield* Scope.make("sequential");
@@ -74248,6 +74591,7 @@ const CodexDriver = {
74248
74591
  const adapter = yield* makeCodexAdapter(effectiveConfig, {
74249
74592
  instanceId,
74250
74593
  environment: processEnv,
74594
+ resolveMcpServers: (yield* McpRegistry).resolveForSession,
74251
74595
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
74252
74596
  });
74253
74597
  const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv);
@@ -79840,6 +80184,31 @@ const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(function*
79840
80184
  };
79841
80185
  });
79842
80186
  //#endregion
80187
+ //#region src/provider/acp/acpMcpServers.ts
80188
+ const namedPairs = (values) => Object.entries(values).map(([name, value]) => ({
80189
+ name,
80190
+ value
80191
+ }));
80192
+ /**
80193
+ * @param resolved - Servers keyed by name, as the registry resolved them.
80194
+ * @param exclude - Names p4code declares itself, which win over a registration
80195
+ * of the same name. The built-in `p4-code` server carries a per-thread
80196
+ * credential no registration can reproduce, so a same-named registration must
80197
+ * not be able to displace it — it would take `task_current` with it, and the
80198
+ * failure would look like the agent forgetting which task it is on.
80199
+ */
80200
+ const toAcpMcpServers = (resolved, exclude = /* @__PURE__ */ new Set()) => Object.entries(resolved).filter(([name]) => !exclude.has(name)).map(([name, server]) => server.type === "stdio" ? {
80201
+ name,
80202
+ command: server.command,
80203
+ args: [...server.args],
80204
+ env: namedPairs(server.env)
80205
+ } : {
80206
+ type: server.type,
80207
+ name,
80208
+ url: server.url,
80209
+ headers: namedPairs(server.headers)
80210
+ });
80211
+ //#endregion
79843
80212
  //#region src/provider/acp/AcpAdapterSupport.ts
79844
80213
  const isAcpProcessExitedError = Schema$1.is(AcpProcessExitedError);
79845
80214
  const isAcpRequestError = Schema$1.is(AcpRequestError);
@@ -80324,6 +80693,15 @@ function makeCursorAdapter(cursorSettings, options) {
80324
80693
  });
80325
80694
  const effectiveCursorSettings = options?.resolveSettings ? yield* options.resolveSettings : cursorSettings;
80326
80695
  const mcpSession = readMcpProviderSession(input.threadId);
80696
+ const mcpServers = [...toAcpMcpServers(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, P4CODE_MCP_SERVER_NAMES), ...mcpSession ? [{
80697
+ type: "http",
80698
+ name: P4CODE_MCP_SERVER_NAME,
80699
+ url: mcpSession.endpoint,
80700
+ headers: [{
80701
+ name: "Authorization",
80702
+ value: mcpSession.authorizationHeader
80703
+ }]
80704
+ }] : []];
80327
80705
  const acp = yield* makeCursorAcpRuntime({
80328
80706
  cursorSettings: effectiveCursorSettings,
80329
80707
  ...options?.environment ? { environment: options.environment } : {},
@@ -80331,18 +80709,10 @@ function makeCursorAdapter(cursorSettings, options) {
80331
80709
  cwd,
80332
80710
  ...resumeSessionId ? { resumeSessionId } : {},
80333
80711
  clientInfo: {
80334
- name: "p4-code",
80712
+ name: P4CODE_MCP_SERVER_NAME,
80335
80713
  version: "0.0.0"
80336
80714
  },
80337
- ...mcpSession ? { mcpServers: [{
80338
- type: "http",
80339
- name: "p4-code",
80340
- url: mcpSession.endpoint,
80341
- headers: [{
80342
- name: "Authorization",
80343
- value: mcpSession.authorizationHeader
80344
- }]
80345
- }] } : {},
80715
+ ...mcpServers.length > 0 ? { mcpServers } : {},
80346
80716
  ...acpNativeLoggers
80347
80717
  }).pipe(Effect.provideService(Crypto.Crypto, crypto), Effect.provideService(Scope.Scope, sessionScope), Effect.mapError((cause) => new ProviderAdapterProcessError({
80348
80718
  provider: PROVIDER$3,
@@ -80823,6 +81193,7 @@ const CursorDriver = {
80823
81193
  });
80824
81194
  const adapter = yield* makeCursorAdapter(effectiveConfig, {
80825
81195
  environment: processEnv,
81196
+ resolveMcpServers: (yield* McpRegistry).resolveForSession,
80826
81197
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {},
80827
81198
  instanceId
80828
81199
  });
@@ -81545,6 +81916,15 @@ function makeGrokAdapter(grokSettings, options) {
81545
81916
  threadId: input.threadId
81546
81917
  });
81547
81918
  const mcpSession = readMcpProviderSession(input.threadId);
81919
+ const mcpServers = [...toAcpMcpServers(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, P4CODE_MCP_SERVER_NAMES), ...mcpSession ? [{
81920
+ type: "http",
81921
+ name: P4CODE_MCP_SERVER_NAME,
81922
+ url: mcpSession.endpoint,
81923
+ headers: [{
81924
+ name: "Authorization",
81925
+ value: mcpSession.authorizationHeader
81926
+ }]
81927
+ }] : []];
81548
81928
  const acp = yield* makeGrokAcpRuntime({
81549
81929
  grokSettings,
81550
81930
  ...options?.environment ? { environment: options.environment } : {},
@@ -81552,18 +81932,10 @@ function makeGrokAdapter(grokSettings, options) {
81552
81932
  cwd,
81553
81933
  ...resumeSessionId ? { resumeSessionId } : {},
81554
81934
  clientInfo: {
81555
- name: "p4-code",
81935
+ name: P4CODE_MCP_SERVER_NAME,
81556
81936
  version: "0.0.0"
81557
81937
  },
81558
- ...mcpSession ? { mcpServers: [{
81559
- type: "http",
81560
- name: "p4-code",
81561
- url: mcpSession.endpoint,
81562
- headers: [{
81563
- name: "Authorization",
81564
- value: mcpSession.authorizationHeader
81565
- }]
81566
- }] } : {},
81938
+ ...mcpServers.length > 0 ? { mcpServers } : {},
81567
81939
  ...acpNativeLoggers
81568
81940
  }).pipe(Effect.provideService(Crypto.Crypto, crypto), Effect.provideService(Scope.Scope, sessionScope), Effect.mapError((cause) => new ProviderAdapterProcessError({
81569
81941
  provider: PROVIDER$2,
@@ -82379,6 +82751,7 @@ const GrokDriver = {
82379
82751
  });
82380
82752
  const adapter = yield* makeGrokAdapter(effectiveConfig, {
82381
82753
  environment: processEnv,
82754
+ resolveMcpServers: (yield* McpRegistry).resolveForSession,
82382
82755
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {},
82383
82756
  instanceId
82384
82757
  });
@@ -84216,6 +84589,27 @@ const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration")(funct
84216
84589
  };
84217
84590
  });
84218
84591
  //#endregion
84592
+ //#region src/provider/Layers/openCodeMcpServers.ts
84593
+ /**
84594
+ * @param resolved - Servers keyed by name, as the registry resolved them.
84595
+ * @param exclude - Names p4code declares itself, which a registration must not
84596
+ * be able to displace: the built-in `p4-code` server carries the per-thread
84597
+ * credential behind `task_current` and the thread tools.
84598
+ */
84599
+ const toOpenCodeMcpServers = (resolved, exclude = /* @__PURE__ */ new Set()) => Object.entries(resolved).filter(([name]) => !exclude.has(name)).map(([name, server]) => ({
84600
+ name,
84601
+ config: server.type === "stdio" ? {
84602
+ type: "local",
84603
+ command: [server.command, ...server.args],
84604
+ ...Object.keys(server.env).length > 0 ? { environment: server.env } : {}
84605
+ } : {
84606
+ type: "remote",
84607
+ url: server.url,
84608
+ ...Object.keys(server.headers).length > 0 ? { headers: server.headers } : {},
84609
+ oauth: false
84610
+ }
84611
+ }));
84612
+ //#endregion
84219
84613
  //#region src/provider/Layers/OpenCodeAdapter.ts
84220
84614
  const PROVIDER = ProviderDriverKind.make("opencode");
84221
84615
  /**
@@ -84892,8 +85286,15 @@ function makeOpenCodeAdapter(openCodeSettings, options) {
84892
85286
  ...server.external && serverPassword ? { serverPassword } : {}
84893
85287
  });
84894
85288
  const mcpSession = readMcpProviderSession(input.threadId);
85289
+ if (!server.external) {
85290
+ const externalMcpServers = toOpenCodeMcpServers(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, P4CODE_MCP_SERVER_NAMES);
85291
+ for (const entry of externalMcpServers) yield* runOpenCodeSdk("mcp.add", () => client.mcp.add(entry)).pipe(Effect.catchCause((cause) => Effect.logWarning("opencode rejected an mcp registration", {
85292
+ server: entry.name,
85293
+ cause
85294
+ })));
85295
+ }
84895
85296
  if (mcpSession && !server.external) yield* runOpenCodeSdk("mcp.add", () => client.mcp.add({
84896
- name: "p4-code",
85297
+ name: P4CODE_MCP_SERVER_NAME,
84897
85298
  config: {
84898
85299
  type: "remote",
84899
85300
  url: mcpSession.endpoint,
@@ -85582,6 +85983,7 @@ const BUILT_IN_DRIVERS = [
85582
85983
  const adapter = yield* makeOpenCodeAdapter(effectiveConfig, {
85583
85984
  instanceId,
85584
85985
  environment: processEnv,
85986
+ resolveMcpServers: (yield* McpRegistry).resolveForSession,
85585
85987
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
85586
85988
  });
85587
85989
  const textGeneration = yield* makeOpenCodeTextGeneration(effectiveConfig, processEnv);
@@ -86254,7 +86656,29 @@ const TaskProposeTool = Tool.make("task_propose", {
86254
86656
  failure: TaskToolError,
86255
86657
  dependencies
86256
86658
  }).annotate(Tool.Title, "Propose a task").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
86257
- const TaskToolkit = Toolkit.make(TaskListTool, TaskGetTool, TaskCurrentTool, TaskCreateTool, TaskUpdateTool, TaskProposeTool);
86659
+ /**
86660
+ * Reading a tracker ticket, for every provider rather than only the ones that
86661
+ * can talk to the tracker themselves.
86662
+ *
86663
+ * p4code resolves this server-side through the Linear server the user
86664
+ * registered once in Settings, so an agent gets the ticket whether or not its
86665
+ * own runtime was handed that registration. Without it, a thread started from
86666
+ * "MOBILE-12241" on a provider with no tracker of its own can only ask the user
86667
+ * to paste the description back in - which is the question they already
86668
+ * answered by quoting the id.
86669
+ *
86670
+ * On the task toolkit rather than a toolkit of its own because it is board
86671
+ * work: a resolved ticket's next step is `task_create` with the `externalRef`
86672
+ * this returns, and the two share the one capability a session is granted.
86673
+ */
86674
+ const TicketResolveTool = Tool.make("ticket_resolve", {
86675
+ description: "Read a tracker ticket by id or URL (e.g. MOBILE-12241, or a linear.app link), returning its identifier, canonical URL, title and description. Use this whenever the user names a ticket rather than asking them to paste it. Pass the returned externalRef straight to task_create to mirror the ticket onto the board. Fails when no tracker is registered on this machine, which is a thing only the user can fix.",
86676
+ parameters: TicketResolveInput,
86677
+ success: TicketResolveResult,
86678
+ failure: Schema$1.Union([TaskToolUnavailableError, TicketResolveError]),
86679
+ dependencies: [McpInvocationContext, TicketResolver]
86680
+ }).annotate(Tool.Title, "Resolve a ticket").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
86681
+ const TaskToolkit = Toolkit.make(TaskListTool, TaskGetTool, TaskCurrentTool, TaskCreateTool, TaskUpdateTool, TaskProposeTool, TicketResolveTool);
86258
86682
  //#endregion
86259
86683
  //#region src/mcp/toolkits/tasks/handlers.ts
86260
86684
  const nowIso$1 = Effect.map(DateTime.now, DateTime.formatIso);
@@ -86340,12 +86764,45 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
86340
86764
  if (Option.isNone(patched)) return yield* new TaskNotFoundError({ taskId: input.taskId });
86341
86765
  return patched.value;
86342
86766
  }),
86767
+ ticket_resolve: (input) => Effect.gen(function* () {
86768
+ yield* requireTaskCapability();
86769
+ return yield* (yield* TicketResolver).resolve(input.reference);
86770
+ }),
86343
86771
  task_propose: () => Effect.gen(function* () {
86344
86772
  yield* requireTaskCapability();
86345
86773
  return { awaitingConfirmation: true };
86346
86774
  })
86347
86775
  });
86348
86776
  //#endregion
86777
+ //#region src/orchestration/pendingThreadSettles.ts
86778
+ /**
86779
+ * Threads that asked to settle while their session was still running.
86780
+ *
86781
+ * The decider refuses `thread.settle` for a thread whose session is starting or
86782
+ * running, and an agent settling its own thread always calls from inside that
86783
+ * session - so the command it wants can never be the command it sends. The
86784
+ * intent is recorded here instead and replayed when the session stops, which is
86785
+ * what "settle this thread" meant from the agent's side anyway: finish, then
86786
+ * close.
86787
+ *
86788
+ * A module-level set rather than a service because it has to be reachable from
86789
+ * two layer trees that do not otherwise meet - an MCP tool handler on one side,
86790
+ * the provider command reactor on the other - and threading a new service
86791
+ * through both to hold a set of ids is more wiring than the thing is worth.
86792
+ * `McpProviderSession` and the active-registry handle are the same shape for
86793
+ * the same reason. It is intentionally not durable: a queued settle belongs to
86794
+ * a session, and a server restart has already ended every session there was.
86795
+ */
86796
+ const pending = /* @__PURE__ */ new Set();
86797
+ const queueThreadSettle = (threadId) => {
86798
+ pending.add(threadId);
86799
+ };
86800
+ /**
86801
+ * Removes the intent and reports whether there was one, so a caller cannot
86802
+ * settle the same thread twice by reading and then forgetting to clear.
86803
+ */
86804
+ const takeQueuedThreadSettle = (threadId) => pending.delete(threadId);
86805
+ //#endregion
86349
86806
  //#region src/sync/assetCompression.ts
86350
86807
  /**
86351
86808
  * Checking that a compressed asset still says what the original said.
@@ -86584,12 +87041,28 @@ const backupFileName = (input) => `${input.fileName}.${input.atIso.replace(/[:.]
86584
87041
  //#endregion
86585
87042
  //#region src/mcp/toolkits/threads/tools.ts
86586
87043
  /**
86587
- * Self-scoped on purpose: neither tool takes a thread id. The thread acted on
86588
- * is always the one in `McpInvocationScope`, exactly as `task_current` resolves
86589
- * it, so a leaked per-session token cannot reach any other thread. Delete,
86590
- * create and archive are deliberately not exposed; settle could never succeed
86591
- * from here because the decider refuses it while the calling session is alive.
87044
+ * Scoped to what this session started, on purpose.
87045
+ *
87046
+ * A control tool may name a thread, but only one this session created with
87047
+ * `thread_spawn`; omitting the id means the session's own thread, which is the
87048
+ * common case and the one that cannot be got wrong. The grant lives on the
87049
+ * credential in `McpSessionRegistry`, so a leaked per-session token still
87050
+ * reaches only the work that token itself began, and its reach cannot widen
87051
+ * because something changed on the board.
87052
+ *
87053
+ * Delete and archive stay unexposed. Deletion is the one irreversible action in
87054
+ * this file and a thread carries its whole transcript, which is a worse thing
87055
+ * to lose than the task row the task toolkit withheld delete for.
87056
+ *
87057
+ * `thread_spawn` goes one level: a thread that was itself spawned is issued a
87058
+ * credential that cannot spawn, so a loop meets a bound rather than a request
87059
+ * not to loop. `thread_settle` on a live session is queued and applied when the
87060
+ * session stops, because the decider refuses an immediate settle there and a
87061
+ * call from a thread's own session always arrives while it is alive.
87062
+ *
87063
+ * @module mcp/toolkits/threads/tools
86592
87064
  */
87065
+ /** Self-only, and staying that way: `thread_configure` is where a title on another thread is set. */
86593
87066
  const ThreadRenameTool = Tool.make("thread_rename", {
86594
87067
  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.",
86595
87068
  parameters: ThreadRenameInput,
@@ -86601,6 +87074,54 @@ const ThreadRenameTool = Tool.make("thread_rename", {
86601
87074
  Crypto.Crypto
86602
87075
  ]
86603
87076
  }).annotate(Tool.Title, "Rename this thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
87077
+ const ThreadSpawnTool = Tool.make("thread_spawn", {
87078
+ description: "Start a new agent thread and send it a first message, then return its id. Use it to hand a piece of work to a fresh thread - a subtask you just planned, a job that wants its own transcript. The new thread inherits this one's project, model and permission mode unless you say otherwise, and can then be watched with thread_watch_events and adjusted with thread_configure. A thread that was itself started this way cannot start another.",
87079
+ parameters: ThreadSpawnInput,
87080
+ success: ThreadSpawnResult,
87081
+ failure: ThreadControlToolError,
87082
+ dependencies: [
87083
+ McpInvocationContext,
87084
+ McpSessionRegistry,
87085
+ OrchestrationEngineService,
87086
+ ProjectionThreadRepository,
87087
+ Crypto.Crypto
87088
+ ]
87089
+ }).annotate(Tool.Title, "Start a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
87090
+ const ThreadConfigureTool = Tool.make("thread_configure", {
87091
+ description: "Change a thread's settings: title, model, permission mode, interaction mode, compression, and whether it may use subagents without asking. Omit threadId to configure this session's own thread; name one only if this session started it. Every field is optional and an omitted field is left alone; the settings actually changed come back in the result.",
87092
+ parameters: ThreadConfigureInput,
87093
+ success: ThreadConfigureResult,
87094
+ failure: ThreadControlToolError,
87095
+ dependencies: [
87096
+ McpInvocationContext,
87097
+ OrchestrationEngineService,
87098
+ Crypto.Crypto
87099
+ ]
87100
+ }).annotate(Tool.Title, "Configure a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
87101
+ const ThreadSettleTool = Tool.make("thread_settle", {
87102
+ description: "Settle a thread, marking its work finished so it drops out of the active list. Omit threadId for this session's own thread; name one only if this session started it. A thread whose session is still running cannot be settled on the spot, so the request is recorded and applied when that session stops - the result says which of the two happened.",
87103
+ parameters: ThreadSettleInput,
87104
+ success: ThreadSettleResult,
87105
+ failure: ThreadControlToolError,
87106
+ dependencies: [
87107
+ McpInvocationContext,
87108
+ OrchestrationEngineService,
87109
+ ProjectionThreadRepository,
87110
+ ProjectionThreadSessionRepository,
87111
+ Crypto.Crypto
87112
+ ]
87113
+ }).annotate(Tool.Title, "Settle a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
87114
+ const ThreadSnoozeTool = Tool.make("thread_snooze", {
87115
+ description: "Snooze a thread until a given time, hiding it from the active list until then. Omit threadId for this session's own thread; name one only if this session started it.",
87116
+ parameters: ThreadSnoozeInput,
87117
+ success: ThreadSnoozeResult,
87118
+ failure: ThreadControlToolError,
87119
+ dependencies: [
87120
+ McpInvocationContext,
87121
+ OrchestrationEngineService,
87122
+ Crypto.Crypto
87123
+ ]
87124
+ }).annotate(Tool.Title, "Snooze a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
86604
87125
  const MemoryAppendTool = Tool.make("memory_append", {
86605
87126
  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.",
86606
87127
  parameters: MemoryAppendInput,
@@ -86636,7 +87157,7 @@ const AssetCompressTool = Tool.make("asset_compress", {
86636
87157
  Path.Path
86637
87158
  ]
86638
87159
  }).annotate(Tool.Title, "Compress an asset").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, false);
86639
- const ThreadToolkit = Toolkit.make(ThreadRenameTool, MemoryAppendTool, AssetCompressTool);
87160
+ const ThreadToolkit = Toolkit.make(ThreadSpawnTool, ThreadConfigureTool, ThreadSettleTool, ThreadSnoozeTool, ThreadRenameTool, MemoryAppendTool, AssetCompressTool);
86640
87161
  //#endregion
86641
87162
  //#region src/mcp/toolkits/threads/handlers.ts
86642
87163
  const DEFAULT_MEMORY_APPEND_TARGET = "CLAUDE.md";
@@ -86653,7 +87174,181 @@ const COMPRESS_BACKUP_DIRNAME = "asset-compress-backups";
86653
87174
  const COMPRESSIBLE_EXTENSION = ".md";
86654
87175
  /** The same test the asset readers use: a NUL is what breaks a JSON text column. */
86655
87176
  const looksBinary = (content) => content.includes("\0");
87177
+ /**
87178
+ * Every control command shares this: mint an id, dispatch, and turn a decider
87179
+ * refusal into one error the agent can read.
87180
+ */
87181
+ const dispatchControl = Effect.fn("mcp.threads.dispatchControl")(function* (command, threadId) {
87182
+ yield* (yield* OrchestrationEngineService).dispatch(command).pipe(Effect.mapError((cause) => new ThreadControlRejectedError({
87183
+ threadId,
87184
+ commandType: command.type,
87185
+ detail: cause.message
87186
+ })));
87187
+ });
87188
+ const newCommandId = Effect.gen(function* () {
87189
+ const crypto = yield* Crypto.Crypto;
87190
+ return CommandId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
87191
+ });
86656
87192
  const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
87193
+ thread_spawn: (input) => Effect.gen(function* () {
87194
+ const invocation = yield* requireThreadSpawn();
87195
+ const registry = yield* McpSessionRegistry;
87196
+ const threads = yield* ProjectionThreadRepository;
87197
+ const crypto = yield* Crypto.Crypto;
87198
+ const parent = yield* threads.getById({ threadId: invocation.threadId }).pipe(Effect.mapError((cause) => new ThreadSpawnNotPermittedError({
87199
+ threadId: invocation.threadId,
87200
+ detail: `this thread could not be read to take its defaults from: ${cause.message}`
87201
+ })));
87202
+ if (Option.isNone(parent)) return yield* new ThreadSpawnNotPermittedError({
87203
+ threadId: invocation.threadId,
87204
+ detail: "this thread is not on the board, so there is nothing to start a thread beside"
87205
+ });
87206
+ const template = parent.value;
87207
+ const projectId = input.projectId ?? template.projectId;
87208
+ const runtimeMode = input.runtimeMode ?? template.runtimeMode;
87209
+ const interactionMode = input.interactionMode ?? template.interactionMode;
87210
+ const compressMode = input.compressMode ?? template.compressMode;
87211
+ const unpromptedSubagents = input.unpromptedSubagents ?? template.unpromptedSubagents === 1;
87212
+ const threadId = ThreadId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
87213
+ const createdAt = DateTime.formatIso(yield* DateTime.now);
87214
+ yield* dispatchControl({
87215
+ type: "thread.create",
87216
+ commandId: yield* newCommandId,
87217
+ threadId,
87218
+ projectId,
87219
+ title: input.title,
87220
+ modelSelection: template.modelSelection,
87221
+ runtimeMode,
87222
+ interactionMode,
87223
+ compressMode,
87224
+ unpromptedSubagents,
87225
+ branch: null,
87226
+ worktreePath: null,
87227
+ createdAt
87228
+ }, threadId);
87229
+ yield* registry.recordSpawnedThread({
87230
+ providerSessionId: invocation.providerSessionId,
87231
+ threadId
87232
+ });
87233
+ yield* dispatchControl({
87234
+ type: "thread.turn.start",
87235
+ commandId: yield* newCommandId,
87236
+ threadId,
87237
+ message: {
87238
+ messageId: MessageId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)),
87239
+ role: "user",
87240
+ text: input.prompt,
87241
+ attachments: []
87242
+ },
87243
+ modelSelection: template.modelSelection,
87244
+ titleSeed: input.title,
87245
+ runtimeMode,
87246
+ interactionMode,
87247
+ compressMode,
87248
+ unpromptedSubagents,
87249
+ createdAt
87250
+ }, threadId);
87251
+ return {
87252
+ threadId,
87253
+ projectId,
87254
+ title: input.title
87255
+ };
87256
+ }),
87257
+ thread_configure: (input) => Effect.gen(function* () {
87258
+ const { threadId } = yield* requireThreadControlTarget(input.threadId);
87259
+ const createdAt = DateTime.formatIso(yield* DateTime.now);
87260
+ const applied = [];
87261
+ if (input.title !== void 0 || input.modelSelection !== void 0) {
87262
+ yield* dispatchControl({
87263
+ type: "thread.meta.update",
87264
+ commandId: yield* newCommandId,
87265
+ threadId,
87266
+ ...input.title !== void 0 ? { title: input.title } : {},
87267
+ ...input.modelSelection !== void 0 ? { modelSelection: input.modelSelection } : {}
87268
+ }, threadId);
87269
+ if (input.title !== void 0) applied.push("title");
87270
+ if (input.modelSelection !== void 0) applied.push("modelSelection");
87271
+ }
87272
+ if (input.runtimeMode !== void 0) {
87273
+ yield* dispatchControl({
87274
+ type: "thread.runtime-mode.set",
87275
+ commandId: yield* newCommandId,
87276
+ threadId,
87277
+ runtimeMode: input.runtimeMode,
87278
+ createdAt
87279
+ }, threadId);
87280
+ applied.push("runtimeMode");
87281
+ }
87282
+ if (input.interactionMode !== void 0) {
87283
+ yield* dispatchControl({
87284
+ type: "thread.interaction-mode.set",
87285
+ commandId: yield* newCommandId,
87286
+ threadId,
87287
+ interactionMode: input.interactionMode,
87288
+ createdAt
87289
+ }, threadId);
87290
+ applied.push("interactionMode");
87291
+ }
87292
+ if (input.compressMode !== void 0) {
87293
+ yield* dispatchControl({
87294
+ type: "thread.compress-mode.set",
87295
+ commandId: yield* newCommandId,
87296
+ threadId,
87297
+ compressMode: input.compressMode,
87298
+ createdAt
87299
+ }, threadId);
87300
+ applied.push("compressMode");
87301
+ }
87302
+ if (input.unpromptedSubagents !== void 0) {
87303
+ yield* dispatchControl({
87304
+ type: "thread.unprompted-subagents.set",
87305
+ commandId: yield* newCommandId,
87306
+ threadId,
87307
+ unpromptedSubagents: input.unpromptedSubagents,
87308
+ createdAt
87309
+ }, threadId);
87310
+ applied.push("unpromptedSubagents");
87311
+ }
87312
+ return {
87313
+ threadId,
87314
+ applied
87315
+ };
87316
+ }),
87317
+ thread_settle: (input) => Effect.gen(function* () {
87318
+ const { threadId } = yield* requireThreadControlTarget(input.threadId);
87319
+ const threads = yield* ProjectionThreadRepository;
87320
+ const session = yield* (yield* ProjectionThreadSessionRepository).getByThreadId({ threadId }).pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
87321
+ if (!(Option.isSome(session) && (session.value.status === "starting" || session.value.status === "running"))) {
87322
+ yield* dispatchControl({
87323
+ type: "thread.settle",
87324
+ commandId: yield* newCommandId,
87325
+ threadId
87326
+ }, threadId);
87327
+ return {
87328
+ threadId,
87329
+ outcome: "settled"
87330
+ };
87331
+ }
87332
+ queueThreadSettle(threadId);
87333
+ yield* threads.getById({ threadId }).pipe(Effect.orDie);
87334
+ return {
87335
+ threadId,
87336
+ outcome: "queued"
87337
+ };
87338
+ }),
87339
+ thread_snooze: (input) => Effect.gen(function* () {
87340
+ const { threadId } = yield* requireThreadControlTarget(input.threadId);
87341
+ yield* dispatchControl({
87342
+ type: "thread.snooze",
87343
+ commandId: yield* newCommandId,
87344
+ threadId,
87345
+ snoozedUntil: input.snoozedUntil
87346
+ }, threadId);
87347
+ return {
87348
+ threadId,
87349
+ snoozedUntil: input.snoozedUntil
87350
+ };
87351
+ }),
86657
87352
  thread_rename: (input) => Effect.gen(function* () {
86658
87353
  const invocation = yield* requireThreadCapability();
86659
87354
  const engine = yield* OrchestrationEngineService;
@@ -88214,13 +88909,34 @@ const make$2 = Effect.gen(function* () {
88214
88909
  if (providerError) return providerError.detail;
88215
88910
  return Cause.pretty(cause);
88216
88911
  };
88912
+ /**
88913
+ * Applies a settle an agent asked for while its session was still running.
88914
+ *
88915
+ * The decider refuses `thread.settle` for a live session, so `thread_settle`
88916
+ * over MCP records the intent instead of failing. This is the moment it
88917
+ * becomes legal: the session has just been written as stopped, so the same
88918
+ * command the agent could not send now succeeds. Taken from the queue rather
88919
+ * than read, so a second stop cannot settle the thread twice.
88920
+ */
88921
+ const flushQueuedSettle = Effect.fnUntraced(function* (threadId) {
88922
+ if (!takeQueuedThreadSettle(threadId)) return;
88923
+ const commandId = yield* serverCommandId("queued-thread-settle");
88924
+ yield* orchestrationEngine.dispatch({
88925
+ type: "thread.settle",
88926
+ commandId,
88927
+ threadId
88928
+ }).pipe(Effect.catchCause((cause) => Effect.logDebug("queued thread settle did not apply", {
88929
+ threadId,
88930
+ cause
88931
+ })));
88932
+ });
88217
88933
  const setThreadSession = (input) => serverCommandId("provider-session-set").pipe(Effect.flatMap((commandId) => orchestrationEngine.dispatch({
88218
88934
  type: "thread.session.set",
88219
88935
  commandId,
88220
88936
  threadId: input.threadId,
88221
88937
  session: input.session,
88222
88938
  createdAt: input.createdAt
88223
- })));
88939
+ })), Effect.tap(() => input.session.status === "stopped" || input.session.status === "error" ? flushQueuedSettle(input.threadId) : Effect.void));
88224
88940
  const setThreadSessionErrorOnTurnStartFailure = Effect.fnUntraced(function* (input) {
88225
88941
  const thread = yield* resolveThread(input.threadId);
88226
88942
  if (!thread) return;