@p4code/cli 0.1.20 → 0.1.21

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 +561 -37
  2. package/dist/client/assets/{DiffPanel-Di8WylWU.js → DiffPanel-2Gqo7b0N.js} +3 -3
  3. package/dist/client/assets/{FilePreviewPanel-CSqqsbYX.js → FilePreviewPanel-Po3D7bHa.js} +5 -5
  4. package/dist/client/assets/PreviewPanel-ebSBxbsf.js +2 -0
  5. package/dist/client/assets/{angular-html-CPSNyy71.js → angular-html-BhHAxMde.js} +2 -2
  6. package/dist/client/assets/{angular-ts-BgndqOdx.js → angular-ts-CxZFyQM4.js} +2 -2
  7. package/dist/client/assets/{apl-CFWK0bta.js → apl-D8rOr4fH.js} +2 -2
  8. package/dist/client/assets/arrow-right-Dseaz2v4.js +2 -0
  9. package/dist/client/assets/{astro-D2uzatlp.js → astro-DYSYdOU9.js} +2 -2
  10. package/dist/client/assets/{blade-DfrK_8z3.js → blade-DaHnYPa_.js} +2 -2
  11. package/dist/client/assets/{c-DyORVYNJ.js → c-oRWOBveW.js} +2 -2
  12. package/dist/client/assets/{cobol-BUWePdBc.js → cobol-Djebq6RA.js} +2 -2
  13. package/dist/client/assets/{coffee-Ba8BYHli.js → coffee-CKhxS88r.js} +2 -2
  14. package/dist/client/assets/{cpp-BX0iAGNA.js → cpp-BjVgWq1h.js} +2 -2
  15. package/dist/client/assets/{crystal-BDSdceoQ.js → crystal-DXzIaEKx.js} +2 -2
  16. package/dist/client/assets/{css-TKd0Sp86.js → css-7AulbBAq.js} +2 -2
  17. package/dist/client/assets/{edge-Bus7Aab8.js → edge-B3VgcX6t.js} +2 -2
  18. package/dist/client/assets/{elixir-BlT2g4EX.js → elixir-DGUU7Wwf.js} +2 -2
  19. package/dist/client/assets/{elm-DTGJZ36e.js → elm-B9tr8qBJ.js} +2 -2
  20. package/dist/client/assets/{erb-BF4yZGxQ.js → erb-BgSl2c_f.js} +2 -2
  21. package/dist/client/assets/{fileCommentAnnotations-C5BlQ5s_.js → fileCommentAnnotations-C06ZwrTh.js} +2 -2
  22. package/dist/client/assets/{git-rebase-GAe0_ZI9.js → git-rebase-CJMhrhmS.js} +2 -2
  23. package/dist/client/assets/{glimmer-js-GWmPSTNt.js → glimmer-js-Dzf5VMSx.js} +2 -2
  24. package/dist/client/assets/{glimmer-ts-CDLqKTZ9.js → glimmer-ts-CHD8gl5-.js} +2 -2
  25. package/dist/client/assets/{glsl-Bv3E3r0O.js → glsl-CDpKClnF.js} +2 -2
  26. package/dist/client/assets/{graphql-B5-LupiK.js → graphql-DX6Ul3G5.js} +2 -2
  27. package/dist/client/assets/{hack-KTQ3DPKm.js → hack-NU-7p0gC.js} +2 -2
  28. package/dist/client/assets/{haml-BvC-52XY.js → haml-Cf7Y-ZgP.js} +2 -2
  29. package/dist/client/assets/{handlebars-CK9WCq20.js → handlebars-BOvzNDfr.js} +2 -2
  30. package/dist/client/assets/{html-BGjxg0hO.js → html-CokKN6Uo.js} +2 -2
  31. package/dist/client/assets/{html-derivative-C0EaHUuc.js → html-derivative-CcTOncd6.js} +2 -2
  32. package/dist/client/assets/{http-hvxfwUee.js → http-CGr88I6S.js} +2 -2
  33. package/dist/client/assets/{hurl-D5cuiBPB.js → hurl-BfdIF20D.js} +2 -2
  34. package/dist/client/assets/index-BwGqPoA8.js +847 -0
  35. package/dist/client/assets/index-eSuLivhH.css +1 -0
  36. package/dist/client/assets/{java-DOkofowB.js → java-DjcrYPKU.js} +2 -2
  37. package/dist/client/assets/{javascript-dCesbfJ7.js → javascript-C1msiELJ.js} +2 -2
  38. package/dist/client/assets/{jinja-BDrVqT_2.js → jinja-BP-z74Sl.js} +2 -2
  39. package/dist/client/assets/{jison-Dvzh0XRq.js → jison-DTU_ib6_.js} +2 -2
  40. package/dist/client/assets/{json-CpV_dVb2.js → json-n2oR8huX.js} +2 -2
  41. package/dist/client/assets/{jsx-CbaKC498.js → jsx-DLc8ZGPF.js} +2 -2
  42. package/dist/client/assets/{julia-BKYdI_5W.js → julia-BFtTfktg.js} +2 -2
  43. package/dist/client/assets/{just-FrVMEGvg.js → just-CBZfmiGm.js} +2 -2
  44. package/dist/client/assets/{latex-BSmwt_7j.js → latex-B9jcgQKp.js} +2 -2
  45. package/dist/client/assets/{liquid-Bb2nFp-U.js → liquid-D-PCb97a.js} +2 -2
  46. package/dist/client/assets/{lua-CEANnQ2c.js → lua-CeAbwjz7.js} +2 -2
  47. package/dist/client/assets/{marko-CjJZ1eHg.js → marko-D2oCpzm8.js} +2 -2
  48. package/dist/client/assets/{mdc-B3VSAYVT.js → mdc-BN4ff7dr.js} +2 -2
  49. package/dist/client/assets/{nginx-BkYJUm4E.js → nginx-TigpgX3q.js} +2 -2
  50. package/dist/client/assets/{nim-D8xT6_xt.js → nim-BbkIxZP5.js} +2 -2
  51. package/dist/client/assets/{perl-cazo7YHC.js → perl-Dq3UNlmA.js} +2 -2
  52. package/dist/client/assets/{php-C0LwuOo3.js → php-CDBbIDUr.js} +2 -2
  53. package/dist/client/assets/{pug-OP6SwNFw.js → pug-X1om8_0C.js} +2 -2
  54. package/dist/client/assets/{qml-C1-WQIQ-.js → qml-C_aweCjl.js} +2 -2
  55. package/dist/client/assets/{r-PkhwsIgx.js → r-BH02pZo7.js} +2 -2
  56. package/dist/client/assets/{razor-WONkd3aR.js → razor-DFd9duVt.js} +2 -2
  57. package/dist/client/assets/{regexp-SDnpa4md.js → regexp-BYgWQcZZ.js} +2 -2
  58. package/dist/client/assets/{rst-DulobrTh.js → rst-DS4ajG1_.js} +2 -2
  59. package/dist/client/assets/{ruby-RYHm8H3x.js → ruby-rrP3E-yX.js} +2 -2
  60. package/dist/client/assets/{sas-BBwb67VZ.js → sas-qOp3AbMp.js} +2 -2
  61. package/dist/client/assets/{scss-y2IZtjKJ.js → scss-BePdknOf.js} +2 -2
  62. package/dist/client/assets/{shellscript-B3vxtpJZ.js → shellscript-Di7DOIUv.js} +2 -2
  63. package/dist/client/assets/{shellsession-CvHfy0TS.js → shellsession-DS-qEJoT.js} +2 -2
  64. package/dist/client/assets/{soy-DWpn3a9x.js → soy-CT_pCK5o.js} +2 -2
  65. package/dist/client/assets/{sql-CCfTsIvj.js → sql-CVynNG6j.js} +2 -2
  66. package/dist/client/assets/{stata-Dr5CN-Tp.js → stata-B2fKxQHN.js} +2 -2
  67. package/dist/client/assets/{surrealql-DgzXJo35.js → surrealql-BOcosx9e.js} +2 -2
  68. package/dist/client/assets/{svelte-JQb5PCjP.js → svelte-BQBQ3ysY.js} +2 -2
  69. package/dist/client/assets/{templ-DQt5AsQ7.js → templ-cinsfbYE.js} +2 -2
  70. package/dist/client/assets/{tex-B8Qokwwv.js → tex-PiJ0CI8J.js} +2 -2
  71. package/dist/client/assets/textarea-DV6gvhvo.js +46 -0
  72. package/dist/client/assets/{ts-tags-B-RrSBzw.js → ts-tags-jRjqE2jk.js} +2 -2
  73. package/dist/client/assets/{tsx-C7Q0nn_B.js → tsx-DqzTbomV.js} +2 -2
  74. package/dist/client/assets/{twig-CzVGXsz7.js → twig-5U1Vd9zl.js} +2 -2
  75. package/dist/client/assets/{typescript-irwEvPgw.js → typescript-DmYNCX8k.js} +2 -2
  76. package/dist/client/assets/{vue-BE-nMa-3.js → vue-BOE7yHne.js} +2 -2
  77. package/dist/client/assets/{vue-html-SXHQOmAo.js → vue-html-roHe5KnN.js} +2 -2
  78. package/dist/client/assets/{vue-vine-D62pnm9I.js → vue-vine-YC-R0SEx.js} +2 -2
  79. package/dist/client/assets/{xml-BnssPo1i.js → xml-B-QQaugn.js} +2 -2
  80. package/dist/client/assets/{xsl-Dg7cN1LO.js → xsl-CyPKxPW7.js} +2 -2
  81. package/dist/client/assets/{yaml-BAeQKrXU.js → yaml-Bq2pe1E4.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.21";
237
237
  //#endregion
238
238
  //#region src/config.ts
239
239
  /**
@@ -7889,18 +7889,35 @@ var TicketResolveError = class extends Schema$1.TaggedErrorClass()("TicketResolv
7889
7889
  //#endregion
7890
7890
  //#region ../../packages/contracts/src/threadControl.ts
7891
7891
  /**
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.
7892
+ * Thread control over MCP - the toolkit an agent uses to act on its own thread
7893
+ * and on the threads it started.
7894
+ *
7895
+ * **The scope rule.** A per-session bearer token is the only thing guarding
7896
+ * `/mcp` on a remote-reachable server, so reach is granted, never assumed. A
7897
+ * session may address its own thread and the threads it created through
7898
+ * `thread_spawn`, and nothing else. A leaked token therefore still reaches only
7899
+ * work that token itself began. The grant is held on the credential rather than
7900
+ * derived from the board, so it cannot widen because someone edited a task.
7901
+ *
7902
+ * **Delete is not exposed, and that is the decision rather than an omission.**
7903
+ * It is the one irreversible action here and a thread carries its whole
7904
+ * transcript. The task toolkit gave the same answer for the same reason, and a
7905
+ * thread is the more expensive thing to lose. An agent that wants a thread gone
7906
+ * settles it and says so; a person deletes it.
7907
+ *
7908
+ * **Spawning is one level deep.** `thread_spawn` seeds a first message and
7909
+ * starts the turn, because a create that cannot say anything is not the feature
7910
+ * anyone asked for. A thread that was itself spawned may not spawn: the depth
7911
+ * cap is what a runaway loop runs into, and one level is enough for "split this
7912
+ * task and start the pieces" while still being a structural bound rather than a
7913
+ * prompt asking nicely.
7914
+ *
7915
+ * **Settle is queued rather than refused.** The decider refuses `thread.settle`
7916
+ * while the target's session is starting or running, and an MCP call from a
7917
+ * thread's own session always arrives while that session is alive - so the
7918
+ * self-addressed case could never succeed as an immediate command. It is
7919
+ * recorded instead and applied when the session stops, which is what an agent
7920
+ * settling its own thread means anyway: finish, then close.
7904
7921
  *
7905
7922
  * Quick-append to memory lives here too: it is the other thing an agent wants
7906
7923
  * to do *from* a thread, and it shares the toolkit's capability and scope.
@@ -7918,6 +7935,64 @@ const ThreadRenameResult = Schema$1.Struct({
7918
7935
  title: TrimmedNonEmptyString
7919
7936
  });
7920
7937
  /**
7938
+ * Which thread a control tool acts on.
7939
+ *
7940
+ * Optional, and absent means the caller's own thread. That default is what
7941
+ * keeps the common case honest: an agent adjusting itself never has to name an
7942
+ * id, so it cannot name the wrong one, and every tool below reads the same way
7943
+ * whether or not it is addressed elsewhere.
7944
+ */
7945
+ 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." }));
7946
+ const ThreadSpawnInput = Schema$1.Struct({
7947
+ title: TrimmedNonEmptyString.annotate({ description: "Title for the new thread. Shown on the board and in the sidebar." }),
7948
+ 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." }),
7949
+ projectId: Schema$1.optional(ProjectId.annotate({ description: "Project to start the thread in. Defaults to this session's own project." })),
7950
+ runtimeMode: Schema$1.optional(RuntimeMode.annotate({ description: "Permission mode for the new thread. Defaults to this session's own." })),
7951
+ interactionMode: Schema$1.optional(ProviderInteractionMode),
7952
+ compressMode: Schema$1.optional(CompressMode),
7953
+ unpromptedSubagents: Schema$1.optional(Schema$1.Boolean)
7954
+ });
7955
+ const ThreadSpawnResult = Schema$1.Struct({
7956
+ /** Watchable with `thread_watch_events`, and addressable by every tool here. */
7957
+ threadId: ThreadId,
7958
+ projectId: ProjectId,
7959
+ title: TrimmedNonEmptyString
7960
+ });
7961
+ const ThreadConfigureInput = Schema$1.Struct({
7962
+ threadId: ControlTargetThreadId,
7963
+ title: Schema$1.optional(TrimmedNonEmptyString),
7964
+ modelSelection: Schema$1.optional(ModelSelection),
7965
+ runtimeMode: Schema$1.optional(RuntimeMode),
7966
+ interactionMode: Schema$1.optional(ProviderInteractionMode),
7967
+ compressMode: Schema$1.optional(CompressMode),
7968
+ unpromptedSubagents: Schema$1.optional(Schema$1.Boolean)
7969
+ });
7970
+ const ThreadConfigureResult = Schema$1.Struct({
7971
+ threadId: ThreadId,
7972
+ /** The settings that were actually changed, in the order they were applied. */
7973
+ applied: Schema$1.Array(Schema$1.String)
7974
+ });
7975
+ const ThreadSettleInput = Schema$1.Struct({ threadId: ControlTargetThreadId });
7976
+ /**
7977
+ * Settled now, or recorded to settle when the session stops.
7978
+ *
7979
+ * Two outcomes rather than one because the difference is the agent's to know: a
7980
+ * queued settle has not happened yet, and a thread that goes on to do more work
7981
+ * still settles at the end of it.
7982
+ */
7983
+ const ThreadSettleResult = Schema$1.Struct({
7984
+ threadId: ThreadId,
7985
+ outcome: Schema$1.Literals(["settled", "queued"])
7986
+ });
7987
+ const ThreadSnoozeInput = Schema$1.Struct({
7988
+ threadId: ControlTargetThreadId,
7989
+ snoozedUntil: IsoDateTime.annotate({ description: "When the thread should come back, as an ISO 8601 timestamp." })
7990
+ });
7991
+ const ThreadSnoozeResult = Schema$1.Struct({
7992
+ threadId: ThreadId,
7993
+ snoozedUntil: IsoDateTime
7994
+ });
7995
+ /**
7921
7996
  * Which memory file an append lands in.
7922
7997
  *
7923
7998
  * A closed union rather than a filename, because the target directory also
@@ -8012,8 +8087,42 @@ var AssetCompressValidationFailedError = class extends Schema$1.TaggedErrorClass
8012
8087
  return `The compressed ${this.path} did not validate, so nothing was written: ${this.errors.join(" ")}`;
8013
8088
  }
8014
8089
  };
8090
+ /**
8091
+ * The named thread is outside this session's grant.
8092
+ *
8093
+ * Deliberately the same answer whether the thread does not exist or exists and
8094
+ * belongs to someone else: a caller that could tell those apart could map the
8095
+ * whole board one id at a time.
8096
+ */
8097
+ var ThreadControlNotPermittedError = class extends Schema$1.TaggedErrorClass()("ThreadControlNotPermittedError", { threadId: ThreadId }) {
8098
+ get message() {
8099
+ 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.`;
8100
+ }
8101
+ };
8102
+ /** A spawned thread asked to spawn. One level is the whole allowance. */
8103
+ var ThreadSpawnNotPermittedError = class extends Schema$1.TaggedErrorClass()("ThreadSpawnNotPermittedError", {
8104
+ threadId: ThreadId,
8105
+ detail: Schema$1.String
8106
+ }) {
8107
+ get message() {
8108
+ return `Thread ${this.threadId} cannot start another thread: ${this.detail}`;
8109
+ }
8110
+ };
8111
+ /** The orchestration engine declined or failed a thread control command. */
8112
+ var ThreadControlRejectedError = class extends Schema$1.TaggedErrorClass()("ThreadControlRejectedError", {
8113
+ threadId: ThreadId,
8114
+ commandType: Schema$1.String,
8115
+ detail: Schema$1.String
8116
+ }) {
8117
+ get message() {
8118
+ return `${this.commandType} on thread ${this.threadId} failed: ${this.detail}`;
8119
+ }
8120
+ };
8015
8121
  const ThreadControlToolError = Schema$1.Union([
8016
8122
  ThreadToolUnavailableError,
8123
+ ThreadControlNotPermittedError,
8124
+ ThreadSpawnNotPermittedError,
8125
+ ThreadControlRejectedError,
8017
8126
  ThreadRenameRejectedError,
8018
8127
  MemoryAppendFailedError,
8019
8128
  AssetCompressRefusedError,
@@ -14929,6 +15038,7 @@ const makeTaskRepository = Effect.gen(function* () {
14929
15038
  ...input.patch.projectId !== void 0 ? { projectId: input.patch.projectId } : {},
14930
15039
  ...input.patch.repositoryKey !== void 0 ? { repositoryKey: input.patch.repositoryKey } : {},
14931
15040
  ...input.patch.threadId !== void 0 ? { threadId: input.patch.threadId } : {},
15041
+ ...input.patch.externalRef !== void 0 ? { externalRef: input.patch.externalRef } : {},
14932
15042
  ...input.patch.parentTaskId !== void 0 ? { parentTaskId: input.patch.parentTaskId } : {},
14933
15043
  ...input.patch.title !== void 0 ? { title: input.patch.title } : {},
14934
15044
  ...input.patch.body !== void 0 ? { body: input.patch.body } : {},
@@ -18870,6 +18980,16 @@ function compressTurnReminderFor(mode) {
18870
18980
  */
18871
18981
  const PRESET_MAX_RESULT_ROWS = 20;
18872
18982
  /**
18983
+ * The tool calls a preset run is allowed.
18984
+ *
18985
+ * Fifteen because a located answer typically takes six to ten, and the runs
18986
+ * that overshoot do not overshoot slightly: measured against real transcripts,
18987
+ * the long tail sits between sixty and a hundred and thirty calls and carries
18988
+ * nearly all of the cost. A cap that a good run never reaches is a cap that only
18989
+ * ever bites the runs that had stopped making progress.
18990
+ */
18991
+ const PRESET_MAX_TOOL_CALLS = 15;
18992
+ /**
18873
18993
  * Named `p4-*` on purpose.
18874
18994
  *
18875
18995
  * The directory is shared with whatever the user has written by hand, and
@@ -18899,6 +19019,34 @@ names a path that is not there, answer with the BLOCKED line below. Guessing at
18899
19019
  the missing half of a request produces an answer that reads as confident and is
18900
19020
  not checkable, which is worse than no answer.`;
18901
19021
  /**
19022
+ * What every preset says about the run in between.
19023
+ *
19024
+ * Three rules, each aimed at a measured failure. The cap bounds the quadratic.
19025
+ * The ban on nesting stops a bounded run from spawning an unbounded one, which
19026
+ * is the same bill again with nothing watching it. The stop-at-the-cap clause is
19027
+ * what makes the number safe to enforce: without somewhere to put a partial
19028
+ * answer, a capped subagent either grinds past the cap or throws away what it
19029
+ * had, and both are worse than a short table with a named gap.
19030
+ */
19031
+ const WORK_BUDGET = `## What the run costs
19032
+
19033
+ At most ${PRESET_MAX_TOOL_CALLS} tool calls. Most answers of this kind take six
19034
+ to ten, so treat the cap as the point where something has gone wrong rather than
19035
+ as a target to spend.
19036
+
19037
+ Spend them on calls that can fail usefully: before each one, know what a hit and
19038
+ a miss would each tell you. Batch searches into one call separated by echoed
19039
+ markers rather than issuing them one at a time, bound every output with a head
19040
+ or a line count, and read a file only after a search has told you which lines
19041
+ matter, and read those lines. Never repeat a call you have already made.
19042
+
19043
+ Do not spawn a subagent of your own. You are the bounded run; one that starts
19044
+ another has spent the budget twice with nothing watching the second half.
19045
+
19046
+ Reaching the cap ends the run. Return the rows you have verified, and add a
19047
+ final line naming the search you would have run next. With no verified rows at
19048
+ all, that is the BLOCKED line.`;
19049
+ /**
18902
19050
  * What every preset says about the answer.
18903
19051
  *
18904
19052
  * The two sentinels matter as much as the table. Without `NONE` a clean run
@@ -18917,7 +19065,8 @@ const outputContract = (row, extra) => [
18917
19065
  ` ${row}`,
18918
19066
  "",
18919
19067
  "Nothing to report: the single word NONE.",
18920
- "Request insufficient: BLOCKED - <what is missing>, on one line.",
19068
+ "Request insufficient, or the budget spent before anything was verified:",
19069
+ "BLOCKED - <what is missing>, on one line.",
18921
19070
  "",
18922
19071
  "Paths are repository-relative and carry a line number. A row without one is",
18923
19072
  "a row you have not verified: open the file and get it.",
@@ -18929,6 +19078,11 @@ const outputContract = (row, extra) => [
18929
19078
  * The compression ruleset goes last so the contract above it is read first.
18930
19079
  * Both are instructions to the same model and neither overrides the other:
18931
19080
  * the contract fixes the shape, the ruleset fixes the prose inside it.
19081
+ *
19082
+ * `model` is optional, and omitting it inherits the session's. Naming a cheaper
19083
+ * tier is worth it only where the job is mechanical enough that the tier is not
19084
+ * what makes the answer right, which is true of locating a symbol and is not
19085
+ * true of deciding whether a line is a bug.
18932
19086
  */
18933
19087
  const buildPreset = (input) => {
18934
19088
  const name = `${PRESET_NAME_PREFIX}${input.name}`;
@@ -18937,6 +19091,7 @@ const buildPreset = (input) => {
18937
19091
  `name: ${name}`,
18938
19092
  `description: ${JSON.stringify(input.description)}`,
18939
19093
  `tools: ${input.tools}`,
19094
+ ...input.model === void 0 ? [] : [`model: ${input.model}`],
18940
19095
  "---",
18941
19096
  "",
18942
19097
  input.body.trim(),
@@ -18959,12 +19114,15 @@ const AGENT_PRESETS = [buildPreset({
18959
19114
  name: "investigator",
18960
19115
  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
19116
  tools: "Read, Grep, Glob, Bash",
19117
+ model: "sonnet",
18962
19118
  summary: "Locates code and answers with a path:line table.",
18963
19119
  body: `You locate code. You do not change it, and you say no more about any location
18964
19120
  than the one clause that explains why it is in the list.
18965
19121
 
18966
19122
  ${INPUT_CONTRACT}
18967
19123
 
19124
+ ${WORK_BUDGET}
19125
+
18968
19126
  ${outputContract("path:line - symbol - one clause on why it matters", [
18969
19127
  "",
18970
19128
  "Symbol is the function, type or constant the line belongs to, not a paraphrase",
@@ -18982,6 +19140,8 @@ approval is a list that has to be re-read to find the three rows that matter.
18982
19140
 
18983
19141
  ${INPUT_CONTRACT}
18984
19142
 
19143
+ ${WORK_BUDGET}
19144
+
18985
19145
  ${outputContract("path:line - severity - what is wrong, and what it breaks", [
18986
19146
  "",
18987
19147
  "Severity is one of three words:",
@@ -38572,6 +38732,38 @@ const requireThreadCapability = Effect.fn("mcp.requireThreadCapability")(functio
38572
38732
  });
38573
38733
  return invocation;
38574
38734
  });
38735
+ /**
38736
+ * The thread a control tool may act on, given what it was asked to act on.
38737
+ *
38738
+ * Two checks like `requireWatchCapability`, and for the same reason: the
38739
+ * capability says this session controls threads at all, the grant says which.
38740
+ * An omitted id resolves to the session's own thread, which is always in the
38741
+ * grant and is why the common call needs no id and so cannot name a wrong one.
38742
+ */
38743
+ const requireThreadControlTarget = Effect.fn("mcp.requireThreadControlTarget")(function* (requested) {
38744
+ const invocation = yield* requireThreadCapability();
38745
+ if (requested === void 0 || requested === invocation.threadId) return {
38746
+ invocation,
38747
+ threadId: invocation.threadId
38748
+ };
38749
+ if (!invocation.controlThreadIds?.has(requested)) return yield* new ThreadControlNotPermittedError({ threadId: requested });
38750
+ return {
38751
+ invocation,
38752
+ threadId: requested
38753
+ };
38754
+ });
38755
+ /**
38756
+ * The guard on starting a thread, which is a different question from acting on
38757
+ * one: it is not "which thread" but "may this session make more of them".
38758
+ */
38759
+ const requireThreadSpawn = Effect.fn("mcp.requireThreadSpawn")(function* () {
38760
+ const invocation = yield* requireThreadCapability();
38761
+ if (invocation.mayCreateThreads !== true) return yield* new ThreadSpawnNotPermittedError({
38762
+ threadId: invocation.threadId,
38763
+ detail: "this thread was itself started by an agent, and spawning goes one level deep so a runaway loop has a bound"
38764
+ });
38765
+ return invocation;
38766
+ });
38575
38767
  //#endregion
38576
38768
  //#region src/mcp/PreviewAutomationBroker.ts
38577
38769
  var PreviewAutomationBroker = class extends Context.Service()("@p4code/cli/mcp/PreviewAutomationBroker") {};
@@ -46688,6 +46880,7 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
46688
46880
  ...row.projectId === null ? {} : { projectId: row.projectId },
46689
46881
  ...row.repositoryKey === null ? {} : { repositoryKey: row.repositoryKey },
46690
46882
  ...row.threadId === null ? {} : { threadId: row.threadId },
46883
+ ...row.externalRef === null ? {} : { externalRef: row.externalRef },
46691
46884
  ...row.parentTaskId === null ? {} : { parentTaskId: row.parentTaskId },
46692
46885
  title: row.title,
46693
46886
  body: row.body,
@@ -47576,7 +47769,10 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
47576
47769
  const crypto = yield* Crypto.Crypto;
47577
47770
  const environmentId = yield* (yield* ServerEnvironment).getEnvironmentId;
47578
47771
  const httpServer = yield* HttpServer.HttpServer;
47579
- const state = yield* SynchronizedRef.make({ records: /* @__PURE__ */ new Map() });
47772
+ const state = yield* SynchronizedRef.make({
47773
+ records: /* @__PURE__ */ new Map(),
47774
+ spawnedThreadIds: /* @__PURE__ */ new Set()
47775
+ });
47580
47776
  const currentTimeMillis = options.now ? Effect.sync(options.now) : Clock.currentTimeMillis;
47581
47777
  const livenessWindowMs = options.livenessWindowMs ?? DEFAULT_LIVENESS_WINDOW_MS;
47582
47778
  const endpoint = httpServer.address._tag === "TcpAddress" ? `http://${getHttpMcpEndpointHost(httpServer.address.hostname)}:${httpServer.address.port}/mcp` : "http://127.0.0.1/mcp";
@@ -47597,29 +47793,35 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
47597
47793
  "threads"
47598
47794
  ]);
47599
47795
  if (watchThreadIds.size > 0) capabilities.add("watch");
47600
- const scope = {
47796
+ const threadId = ThreadId.make(request.threadId);
47797
+ const scopeWith = (mayCreateThreads) => ({
47601
47798
  environmentId,
47602
- threadId: ThreadId.make(request.threadId),
47799
+ threadId,
47603
47800
  providerSessionId,
47604
47801
  providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),
47605
47802
  capabilities,
47606
47803
  ...watchThreadIds.size > 0 ? { watchThreadIds } : {},
47804
+ mayCreateThreads,
47607
47805
  issuedAt
47608
- };
47609
- yield* SynchronizedRef.update(state, ({ records }) => {
47806
+ });
47807
+ yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
47610
47808
  const next = new Map(pruneDead(records, issuedAt));
47611
47809
  next.set(tokenHash, {
47612
47810
  tokenHash,
47613
- scope,
47811
+ scope: scopeWith(!spawnedThreadIds.has(threadId)),
47812
+ controlThreadIds: /* @__PURE__ */ new Set(),
47614
47813
  lastAliveAt: issuedAt
47615
47814
  });
47616
- return { records: next };
47815
+ return {
47816
+ records: next,
47817
+ spawnedThreadIds
47818
+ };
47617
47819
  });
47618
47820
  return { config: {
47619
47821
  environmentId,
47620
- threadId: scope.threadId,
47822
+ threadId,
47621
47823
  providerSessionId,
47622
- providerInstanceId: scope.providerInstanceId,
47824
+ providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),
47623
47825
  endpoint,
47624
47826
  authorizationHeader: `Bearer ${rawToken}`
47625
47827
  } };
@@ -47628,42 +47830,76 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
47628
47830
  if (rawToken.length === 0) return void 0;
47629
47831
  const tokenHash = yield* hashToken(rawToken);
47630
47832
  const timestamp = yield* currentTimeMillis;
47631
- return yield* SynchronizedRef.modify(state, ({ records }) => {
47833
+ return yield* SynchronizedRef.modify(state, ({ records, spawnedThreadIds }) => {
47632
47834
  const current = pruneDead(records, timestamp);
47633
47835
  const record = current.get(tokenHash);
47634
- if (!record) return [void 0, { records: current }];
47836
+ if (!record) return [void 0, {
47837
+ records: current,
47838
+ spawnedThreadIds
47839
+ }];
47635
47840
  const next = new Map(current);
47636
47841
  next.set(tokenHash, {
47637
47842
  ...record,
47638
47843
  lastAliveAt: timestamp
47639
47844
  });
47640
- return [record.scope, { records: next }];
47845
+ return [record.controlThreadIds.size === 0 ? record.scope : {
47846
+ ...record.scope,
47847
+ capabilities: /* @__PURE__ */ new Set([...record.scope.capabilities, "watch"]),
47848
+ controlThreadIds: record.controlThreadIds,
47849
+ watchThreadIds: /* @__PURE__ */ new Set([...record.scope.watchThreadIds ?? [], ...record.controlThreadIds])
47850
+ }, {
47851
+ records: next,
47852
+ spawnedThreadIds
47853
+ }];
47641
47854
  });
47642
47855
  });
47643
47856
  const touch = Effect.fn("McpSessionRegistry.touch")(function* (threadId) {
47644
47857
  const timestamp = yield* currentTimeMillis;
47645
- yield* SynchronizedRef.update(state, ({ records }) => {
47858
+ yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
47646
47859
  const current = pruneDead(records, timestamp);
47647
47860
  const next = new Map(current);
47648
47861
  for (const [tokenHash, record] of current) if (record.scope.threadId === threadId) next.set(tokenHash, {
47649
47862
  ...record,
47650
47863
  lastAliveAt: timestamp
47651
47864
  });
47652
- return { records: next };
47865
+ return {
47866
+ records: next,
47867
+ spawnedThreadIds
47868
+ };
47869
+ });
47870
+ });
47871
+ const recordSpawnedThread = Effect.fn("McpSessionRegistry.recordSpawnedThread")(function* (input) {
47872
+ yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
47873
+ const next = new Map(records);
47874
+ for (const [tokenHash, record] of records) if (record.scope.providerSessionId === input.providerSessionId) next.set(tokenHash, {
47875
+ ...record,
47876
+ controlThreadIds: /* @__PURE__ */ new Set([...record.controlThreadIds, input.threadId])
47877
+ });
47878
+ return {
47879
+ records: next,
47880
+ spawnedThreadIds: /* @__PURE__ */ new Set([...spawnedThreadIds, input.threadId])
47881
+ };
47653
47882
  });
47654
47883
  });
47655
- const revokeWhere = (predicate) => SynchronizedRef.update(state, ({ records }) => ({ records: new Map(Array.from(records).filter(([, record]) => !predicate(record))) }));
47884
+ const revokeWhere = (predicate) => SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => ({
47885
+ records: new Map(Array.from(records).filter(([, record]) => !predicate(record))),
47886
+ spawnedThreadIds
47887
+ }));
47656
47888
  return McpSessionRegistry.of({
47657
47889
  issue,
47658
47890
  resolve,
47659
47891
  touch,
47892
+ recordSpawnedThread,
47660
47893
  revokeProviderSession: Effect.fn("McpSessionRegistry.revokeProviderSession")(function* (providerSessionId) {
47661
47894
  yield* revokeWhere((record) => record.scope.providerSessionId === providerSessionId);
47662
47895
  }),
47663
47896
  revokeThread: Effect.fn("McpSessionRegistry.revokeThread")(function* (threadId) {
47664
47897
  yield* revokeWhere((record) => record.scope.threadId === threadId);
47665
47898
  }),
47666
- revokeAll: SynchronizedRef.set(state, { records: /* @__PURE__ */ new Map() })
47899
+ revokeAll: SynchronizedRef.update(state, ({ spawnedThreadIds }) => ({
47900
+ records: /* @__PURE__ */ new Map(),
47901
+ spawnedThreadIds
47902
+ }))
47667
47903
  });
47668
47904
  });
47669
47905
  let activeMcpSessionRegistry;
@@ -86346,6 +86582,35 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
86346
86582
  })
86347
86583
  });
86348
86584
  //#endregion
86585
+ //#region src/orchestration/pendingThreadSettles.ts
86586
+ /**
86587
+ * Threads that asked to settle while their session was still running.
86588
+ *
86589
+ * The decider refuses `thread.settle` for a thread whose session is starting or
86590
+ * running, and an agent settling its own thread always calls from inside that
86591
+ * session - so the command it wants can never be the command it sends. The
86592
+ * intent is recorded here instead and replayed when the session stops, which is
86593
+ * what "settle this thread" meant from the agent's side anyway: finish, then
86594
+ * close.
86595
+ *
86596
+ * A module-level set rather than a service because it has to be reachable from
86597
+ * two layer trees that do not otherwise meet - an MCP tool handler on one side,
86598
+ * the provider command reactor on the other - and threading a new service
86599
+ * through both to hold a set of ids is more wiring than the thing is worth.
86600
+ * `McpProviderSession` and the active-registry handle are the same shape for
86601
+ * the same reason. It is intentionally not durable: a queued settle belongs to
86602
+ * a session, and a server restart has already ended every session there was.
86603
+ */
86604
+ const pending = /* @__PURE__ */ new Set();
86605
+ const queueThreadSettle = (threadId) => {
86606
+ pending.add(threadId);
86607
+ };
86608
+ /**
86609
+ * Removes the intent and reports whether there was one, so a caller cannot
86610
+ * settle the same thread twice by reading and then forgetting to clear.
86611
+ */
86612
+ const takeQueuedThreadSettle = (threadId) => pending.delete(threadId);
86613
+ //#endregion
86349
86614
  //#region src/sync/assetCompression.ts
86350
86615
  /**
86351
86616
  * Checking that a compressed asset still says what the original said.
@@ -86584,12 +86849,28 @@ const backupFileName = (input) => `${input.fileName}.${input.atIso.replace(/[:.]
86584
86849
  //#endregion
86585
86850
  //#region src/mcp/toolkits/threads/tools.ts
86586
86851
  /**
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.
86852
+ * Scoped to what this session started, on purpose.
86853
+ *
86854
+ * A control tool may name a thread, but only one this session created with
86855
+ * `thread_spawn`; omitting the id means the session's own thread, which is the
86856
+ * common case and the one that cannot be got wrong. The grant lives on the
86857
+ * credential in `McpSessionRegistry`, so a leaked per-session token still
86858
+ * reaches only the work that token itself began, and its reach cannot widen
86859
+ * because something changed on the board.
86860
+ *
86861
+ * Delete and archive stay unexposed. Deletion is the one irreversible action in
86862
+ * this file and a thread carries its whole transcript, which is a worse thing
86863
+ * to lose than the task row the task toolkit withheld delete for.
86864
+ *
86865
+ * `thread_spawn` goes one level: a thread that was itself spawned is issued a
86866
+ * credential that cannot spawn, so a loop meets a bound rather than a request
86867
+ * not to loop. `thread_settle` on a live session is queued and applied when the
86868
+ * session stops, because the decider refuses an immediate settle there and a
86869
+ * call from a thread's own session always arrives while it is alive.
86870
+ *
86871
+ * @module mcp/toolkits/threads/tools
86592
86872
  */
86873
+ /** Self-only, and staying that way: `thread_configure` is where a title on another thread is set. */
86593
86874
  const ThreadRenameTool = Tool.make("thread_rename", {
86594
86875
  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
86876
  parameters: ThreadRenameInput,
@@ -86601,6 +86882,54 @@ const ThreadRenameTool = Tool.make("thread_rename", {
86601
86882
  Crypto.Crypto
86602
86883
  ]
86603
86884
  }).annotate(Tool.Title, "Rename this thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
86885
+ const ThreadSpawnTool = Tool.make("thread_spawn", {
86886
+ 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.",
86887
+ parameters: ThreadSpawnInput,
86888
+ success: ThreadSpawnResult,
86889
+ failure: ThreadControlToolError,
86890
+ dependencies: [
86891
+ McpInvocationContext,
86892
+ McpSessionRegistry,
86893
+ OrchestrationEngineService,
86894
+ ProjectionThreadRepository,
86895
+ Crypto.Crypto
86896
+ ]
86897
+ }).annotate(Tool.Title, "Start a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
86898
+ const ThreadConfigureTool = Tool.make("thread_configure", {
86899
+ 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.",
86900
+ parameters: ThreadConfigureInput,
86901
+ success: ThreadConfigureResult,
86902
+ failure: ThreadControlToolError,
86903
+ dependencies: [
86904
+ McpInvocationContext,
86905
+ OrchestrationEngineService,
86906
+ Crypto.Crypto
86907
+ ]
86908
+ }).annotate(Tool.Title, "Configure a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
86909
+ const ThreadSettleTool = Tool.make("thread_settle", {
86910
+ 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.",
86911
+ parameters: ThreadSettleInput,
86912
+ success: ThreadSettleResult,
86913
+ failure: ThreadControlToolError,
86914
+ dependencies: [
86915
+ McpInvocationContext,
86916
+ OrchestrationEngineService,
86917
+ ProjectionThreadRepository,
86918
+ ProjectionThreadSessionRepository,
86919
+ Crypto.Crypto
86920
+ ]
86921
+ }).annotate(Tool.Title, "Settle a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
86922
+ const ThreadSnoozeTool = Tool.make("thread_snooze", {
86923
+ 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.",
86924
+ parameters: ThreadSnoozeInput,
86925
+ success: ThreadSnoozeResult,
86926
+ failure: ThreadControlToolError,
86927
+ dependencies: [
86928
+ McpInvocationContext,
86929
+ OrchestrationEngineService,
86930
+ Crypto.Crypto
86931
+ ]
86932
+ }).annotate(Tool.Title, "Snooze a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
86604
86933
  const MemoryAppendTool = Tool.make("memory_append", {
86605
86934
  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
86935
  parameters: MemoryAppendInput,
@@ -86636,7 +86965,7 @@ const AssetCompressTool = Tool.make("asset_compress", {
86636
86965
  Path.Path
86637
86966
  ]
86638
86967
  }).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);
86968
+ const ThreadToolkit = Toolkit.make(ThreadSpawnTool, ThreadConfigureTool, ThreadSettleTool, ThreadSnoozeTool, ThreadRenameTool, MemoryAppendTool, AssetCompressTool);
86640
86969
  //#endregion
86641
86970
  //#region src/mcp/toolkits/threads/handlers.ts
86642
86971
  const DEFAULT_MEMORY_APPEND_TARGET = "CLAUDE.md";
@@ -86653,7 +86982,181 @@ const COMPRESS_BACKUP_DIRNAME = "asset-compress-backups";
86653
86982
  const COMPRESSIBLE_EXTENSION = ".md";
86654
86983
  /** The same test the asset readers use: a NUL is what breaks a JSON text column. */
86655
86984
  const looksBinary = (content) => content.includes("\0");
86985
+ /**
86986
+ * Every control command shares this: mint an id, dispatch, and turn a decider
86987
+ * refusal into one error the agent can read.
86988
+ */
86989
+ const dispatchControl = Effect.fn("mcp.threads.dispatchControl")(function* (command, threadId) {
86990
+ yield* (yield* OrchestrationEngineService).dispatch(command).pipe(Effect.mapError((cause) => new ThreadControlRejectedError({
86991
+ threadId,
86992
+ commandType: command.type,
86993
+ detail: cause.message
86994
+ })));
86995
+ });
86996
+ const newCommandId = Effect.gen(function* () {
86997
+ const crypto = yield* Crypto.Crypto;
86998
+ return CommandId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
86999
+ });
86656
87000
  const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
87001
+ thread_spawn: (input) => Effect.gen(function* () {
87002
+ const invocation = yield* requireThreadSpawn();
87003
+ const registry = yield* McpSessionRegistry;
87004
+ const threads = yield* ProjectionThreadRepository;
87005
+ const crypto = yield* Crypto.Crypto;
87006
+ const parent = yield* threads.getById({ threadId: invocation.threadId }).pipe(Effect.mapError((cause) => new ThreadSpawnNotPermittedError({
87007
+ threadId: invocation.threadId,
87008
+ detail: `this thread could not be read to take its defaults from: ${cause.message}`
87009
+ })));
87010
+ if (Option.isNone(parent)) return yield* new ThreadSpawnNotPermittedError({
87011
+ threadId: invocation.threadId,
87012
+ detail: "this thread is not on the board, so there is nothing to start a thread beside"
87013
+ });
87014
+ const template = parent.value;
87015
+ const projectId = input.projectId ?? template.projectId;
87016
+ const runtimeMode = input.runtimeMode ?? template.runtimeMode;
87017
+ const interactionMode = input.interactionMode ?? template.interactionMode;
87018
+ const compressMode = input.compressMode ?? template.compressMode;
87019
+ const unpromptedSubagents = input.unpromptedSubagents ?? template.unpromptedSubagents === 1;
87020
+ const threadId = ThreadId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
87021
+ const createdAt = DateTime.formatIso(yield* DateTime.now);
87022
+ yield* dispatchControl({
87023
+ type: "thread.create",
87024
+ commandId: yield* newCommandId,
87025
+ threadId,
87026
+ projectId,
87027
+ title: input.title,
87028
+ modelSelection: template.modelSelection,
87029
+ runtimeMode,
87030
+ interactionMode,
87031
+ compressMode,
87032
+ unpromptedSubagents,
87033
+ branch: null,
87034
+ worktreePath: null,
87035
+ createdAt
87036
+ }, threadId);
87037
+ yield* registry.recordSpawnedThread({
87038
+ providerSessionId: invocation.providerSessionId,
87039
+ threadId
87040
+ });
87041
+ yield* dispatchControl({
87042
+ type: "thread.turn.start",
87043
+ commandId: yield* newCommandId,
87044
+ threadId,
87045
+ message: {
87046
+ messageId: MessageId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)),
87047
+ role: "user",
87048
+ text: input.prompt,
87049
+ attachments: []
87050
+ },
87051
+ modelSelection: template.modelSelection,
87052
+ titleSeed: input.title,
87053
+ runtimeMode,
87054
+ interactionMode,
87055
+ compressMode,
87056
+ unpromptedSubagents,
87057
+ createdAt
87058
+ }, threadId);
87059
+ return {
87060
+ threadId,
87061
+ projectId,
87062
+ title: input.title
87063
+ };
87064
+ }),
87065
+ thread_configure: (input) => Effect.gen(function* () {
87066
+ const { threadId } = yield* requireThreadControlTarget(input.threadId);
87067
+ const createdAt = DateTime.formatIso(yield* DateTime.now);
87068
+ const applied = [];
87069
+ if (input.title !== void 0 || input.modelSelection !== void 0) {
87070
+ yield* dispatchControl({
87071
+ type: "thread.meta.update",
87072
+ commandId: yield* newCommandId,
87073
+ threadId,
87074
+ ...input.title !== void 0 ? { title: input.title } : {},
87075
+ ...input.modelSelection !== void 0 ? { modelSelection: input.modelSelection } : {}
87076
+ }, threadId);
87077
+ if (input.title !== void 0) applied.push("title");
87078
+ if (input.modelSelection !== void 0) applied.push("modelSelection");
87079
+ }
87080
+ if (input.runtimeMode !== void 0) {
87081
+ yield* dispatchControl({
87082
+ type: "thread.runtime-mode.set",
87083
+ commandId: yield* newCommandId,
87084
+ threadId,
87085
+ runtimeMode: input.runtimeMode,
87086
+ createdAt
87087
+ }, threadId);
87088
+ applied.push("runtimeMode");
87089
+ }
87090
+ if (input.interactionMode !== void 0) {
87091
+ yield* dispatchControl({
87092
+ type: "thread.interaction-mode.set",
87093
+ commandId: yield* newCommandId,
87094
+ threadId,
87095
+ interactionMode: input.interactionMode,
87096
+ createdAt
87097
+ }, threadId);
87098
+ applied.push("interactionMode");
87099
+ }
87100
+ if (input.compressMode !== void 0) {
87101
+ yield* dispatchControl({
87102
+ type: "thread.compress-mode.set",
87103
+ commandId: yield* newCommandId,
87104
+ threadId,
87105
+ compressMode: input.compressMode,
87106
+ createdAt
87107
+ }, threadId);
87108
+ applied.push("compressMode");
87109
+ }
87110
+ if (input.unpromptedSubagents !== void 0) {
87111
+ yield* dispatchControl({
87112
+ type: "thread.unprompted-subagents.set",
87113
+ commandId: yield* newCommandId,
87114
+ threadId,
87115
+ unpromptedSubagents: input.unpromptedSubagents,
87116
+ createdAt
87117
+ }, threadId);
87118
+ applied.push("unpromptedSubagents");
87119
+ }
87120
+ return {
87121
+ threadId,
87122
+ applied
87123
+ };
87124
+ }),
87125
+ thread_settle: (input) => Effect.gen(function* () {
87126
+ const { threadId } = yield* requireThreadControlTarget(input.threadId);
87127
+ const threads = yield* ProjectionThreadRepository;
87128
+ const session = yield* (yield* ProjectionThreadSessionRepository).getByThreadId({ threadId }).pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
87129
+ if (!(Option.isSome(session) && (session.value.status === "starting" || session.value.status === "running"))) {
87130
+ yield* dispatchControl({
87131
+ type: "thread.settle",
87132
+ commandId: yield* newCommandId,
87133
+ threadId
87134
+ }, threadId);
87135
+ return {
87136
+ threadId,
87137
+ outcome: "settled"
87138
+ };
87139
+ }
87140
+ queueThreadSettle(threadId);
87141
+ yield* threads.getById({ threadId }).pipe(Effect.orDie);
87142
+ return {
87143
+ threadId,
87144
+ outcome: "queued"
87145
+ };
87146
+ }),
87147
+ thread_snooze: (input) => Effect.gen(function* () {
87148
+ const { threadId } = yield* requireThreadControlTarget(input.threadId);
87149
+ yield* dispatchControl({
87150
+ type: "thread.snooze",
87151
+ commandId: yield* newCommandId,
87152
+ threadId,
87153
+ snoozedUntil: input.snoozedUntil
87154
+ }, threadId);
87155
+ return {
87156
+ threadId,
87157
+ snoozedUntil: input.snoozedUntil
87158
+ };
87159
+ }),
86657
87160
  thread_rename: (input) => Effect.gen(function* () {
86658
87161
  const invocation = yield* requireThreadCapability();
86659
87162
  const engine = yield* OrchestrationEngineService;
@@ -88214,13 +88717,34 @@ const make$2 = Effect.gen(function* () {
88214
88717
  if (providerError) return providerError.detail;
88215
88718
  return Cause.pretty(cause);
88216
88719
  };
88720
+ /**
88721
+ * Applies a settle an agent asked for while its session was still running.
88722
+ *
88723
+ * The decider refuses `thread.settle` for a live session, so `thread_settle`
88724
+ * over MCP records the intent instead of failing. This is the moment it
88725
+ * becomes legal: the session has just been written as stopped, so the same
88726
+ * command the agent could not send now succeeds. Taken from the queue rather
88727
+ * than read, so a second stop cannot settle the thread twice.
88728
+ */
88729
+ const flushQueuedSettle = Effect.fnUntraced(function* (threadId) {
88730
+ if (!takeQueuedThreadSettle(threadId)) return;
88731
+ const commandId = yield* serverCommandId("queued-thread-settle");
88732
+ yield* orchestrationEngine.dispatch({
88733
+ type: "thread.settle",
88734
+ commandId,
88735
+ threadId
88736
+ }).pipe(Effect.catchCause((cause) => Effect.logDebug("queued thread settle did not apply", {
88737
+ threadId,
88738
+ cause
88739
+ })));
88740
+ });
88217
88741
  const setThreadSession = (input) => serverCommandId("provider-session-set").pipe(Effect.flatMap((commandId) => orchestrationEngine.dispatch({
88218
88742
  type: "thread.session.set",
88219
88743
  commandId,
88220
88744
  threadId: input.threadId,
88221
88745
  session: input.session,
88222
88746
  createdAt: input.createdAt
88223
- })));
88747
+ })), Effect.tap(() => input.session.status === "stopped" || input.session.status === "error" ? flushQueuedSettle(input.threadId) : Effect.void));
88224
88748
  const setThreadSessionErrorOnTurnStartFailure = Effect.fnUntraced(function* (input) {
88225
88749
  const thread = yield* resolveThread(input.threadId);
88226
88750
  if (!thread) return;