@p4code/cli 0.0.49 → 0.0.51

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 +796 -47
  2. package/dist/client/assets/{DiffPanel-Byg3lT0_.js → DiffPanel-4ZPfw-bt.js} +3 -3
  3. package/dist/client/assets/{FilePreviewPanel-CRsMuFPM.js → FilePreviewPanel-DPl3V4qz.js} +5 -5
  4. package/dist/client/assets/PreviewPanel-DENxqydi.js +2 -0
  5. package/dist/client/assets/{angular-html-CvITUhcj.js → angular-html-SfFqn_n1.js} +2 -2
  6. package/dist/client/assets/{angular-ts-CI8oh2Bi.js → angular-ts-TqTMcSgN.js} +2 -2
  7. package/dist/client/assets/{apl-BewkNmrv.js → apl-DvTVLZcC.js} +2 -2
  8. package/dist/client/assets/arrow-right-BHEVGFLQ.js +2 -0
  9. package/dist/client/assets/{astro-CNN681lo.js → astro-DEpzE1sb.js} +2 -2
  10. package/dist/client/assets/{blade-X4RszKFf.js → blade-Brgi-xhV.js} +2 -2
  11. package/dist/client/assets/{c-C8AJe2jK.js → c-7H0fCJJT.js} +2 -2
  12. package/dist/client/assets/{cobol-DQjALrRk.js → cobol-Cq_E3Ld9.js} +2 -2
  13. package/dist/client/assets/{coffee-uHbL69mO.js → coffee-DpUX5Xcz.js} +2 -2
  14. package/dist/client/assets/{cpp-D3rb3O4A.js → cpp-BEjZmHLc.js} +2 -2
  15. package/dist/client/assets/{crystal-spyqFjYj.js → crystal-CycEJw4X.js} +2 -2
  16. package/dist/client/assets/{css-CbxXkRaq.js → css-y3Oj4fDi.js} +2 -2
  17. package/dist/client/assets/{edge-YESgrh6X.js → edge-DIFYHFsA.js} +2 -2
  18. package/dist/client/assets/{elixir-7069ioPL.js → elixir-raLzjUMQ.js} +2 -2
  19. package/dist/client/assets/{elm-Eb4zNBLt.js → elm-CZsXcOy1.js} +2 -2
  20. package/dist/client/assets/{erb-Ddb27xlO.js → erb-DhXjIste.js} +2 -2
  21. package/dist/client/assets/{fileCommentAnnotations-DEIuuBiQ.js → fileCommentAnnotations-DkadV_kZ.js} +2 -2
  22. package/dist/client/assets/{git-rebase-CdERns2w.js → git-rebase-CZvi6-AX.js} +2 -2
  23. package/dist/client/assets/{glimmer-js-C6s5Isxr.js → glimmer-js-N_UF0Pmi.js} +2 -2
  24. package/dist/client/assets/{glimmer-ts-BV7NBmEc.js → glimmer-ts-Cxd1tQNi.js} +2 -2
  25. package/dist/client/assets/{glsl-jd9QEV4q.js → glsl-B8RUupmi.js} +2 -2
  26. package/dist/client/assets/{graphql-CXqIl6Zm.js → graphql-Cz2mi8GF.js} +2 -2
  27. package/dist/client/assets/{hack-lLvPp6HR.js → hack-DD3q-ECT.js} +2 -2
  28. package/dist/client/assets/{haml-DfPUmRcz.js → haml-C75rIu1p.js} +2 -2
  29. package/dist/client/assets/{handlebars-QzjSM_We.js → handlebars-C1zz9h1i.js} +2 -2
  30. package/dist/client/assets/{html-derivative-CkX2Wmz3.js → html-derivative-HEAA-Z2a.js} +2 -2
  31. package/dist/client/assets/{html-BySpIsKh.js → html-mXOatteF.js} +2 -2
  32. package/dist/client/assets/{http-CD7dw1li.js → http-DO0ZHyk0.js} +2 -2
  33. package/dist/client/assets/{hurl-nzp7hSsy.js → hurl-DyDQ6Iuv.js} +2 -2
  34. package/dist/client/assets/index-30avYZEg.js +663 -0
  35. package/dist/client/assets/index-C7UXI10J.css +1 -0
  36. package/dist/client/assets/{java-D2TcotRb.js → java-DicQ_x8H.js} +2 -2
  37. package/dist/client/assets/{javascript-CpW0WD7L.js → javascript-CTx-kiCR.js} +2 -2
  38. package/dist/client/assets/{jinja-BL1anCEN.js → jinja-ERQPZ9pQ.js} +2 -2
  39. package/dist/client/assets/{jison-B-sCtmJb.js → jison-BEedW5zW.js} +2 -2
  40. package/dist/client/assets/{json-BXeO84aE.js → json-MMdgCPnh.js} +2 -2
  41. package/dist/client/assets/{jsx-CgueyhKv.js → jsx-CJ--w_e-.js} +2 -2
  42. package/dist/client/assets/{julia-PMog-fIJ.js → julia-BvGAE4O0.js} +2 -2
  43. package/dist/client/assets/{just-Bi73ZAuY.js → just-BvBn61hL.js} +2 -2
  44. package/dist/client/assets/{latex-D6Hw5PyK.js → latex-P2_b6CEl.js} +2 -2
  45. package/dist/client/assets/{liquid-CuvsknsU.js → liquid-Dm2xzeBN.js} +2 -2
  46. package/dist/client/assets/{lua-6tqLQzzk.js → lua-DYQLFKMA.js} +2 -2
  47. package/dist/client/assets/{marko-Bv1DHr8G.js → marko-DCc0fplW.js} +2 -2
  48. package/dist/client/assets/{mdc-DIKkjN2a.js → mdc-BI6uqWRL.js} +2 -2
  49. package/dist/client/assets/{nginx-B2JFBlIu.js → nginx-ijGdX1bh.js} +2 -2
  50. package/dist/client/assets/{nim-D8qC29Zr.js → nim-CUp-JGxu.js} +2 -2
  51. package/dist/client/assets/{perl-De_VpaHg.js → perl-BKpwnNRr.js} +2 -2
  52. package/dist/client/assets/{php-zSI5z7ZS.js → php-DDStFT9W.js} +2 -2
  53. package/dist/client/assets/{pug-rUGxU82h.js → pug-HXcSxf00.js} +2 -2
  54. package/dist/client/assets/{qml-CXAY6cUl.js → qml-CbzgrBYo.js} +2 -2
  55. package/dist/client/assets/{r-Cm15VoHH.js → r-CgMrZP2h.js} +2 -2
  56. package/dist/client/assets/{razor-DkcxdUAy.js → razor-0ZAHbKMz.js} +2 -2
  57. package/dist/client/assets/{regexp-BOilP6p2.js → regexp-Bzi1hKAb.js} +2 -2
  58. package/dist/client/assets/{rst-BfN2oPCh.js → rst-qWyAhT77.js} +2 -2
  59. package/dist/client/assets/{ruby-B1jLvIIs.js → ruby-LCEp4zKA.js} +2 -2
  60. package/dist/client/assets/{sas-CF03ALyS.js → sas-BFhxcd3b.js} +2 -2
  61. package/dist/client/assets/{scss-BrFzdYUJ.js → scss-BK0-84uz.js} +2 -2
  62. package/dist/client/assets/{shellscript-B_6ownRV.js → shellscript-CyZVCH9y.js} +2 -2
  63. package/dist/client/assets/{shellsession-D9grZ02W.js → shellsession-BMyVGgke.js} +2 -2
  64. package/dist/client/assets/{soy-Ca44AVp-.js → soy-C5EqpU05.js} +2 -2
  65. package/dist/client/assets/{sql-CkHELsvP.js → sql-D51Kht_s.js} +2 -2
  66. package/dist/client/assets/{stata-C9ALGlol.js → stata-ClgcFk9O.js} +2 -2
  67. package/dist/client/assets/{surrealql-Cta1X2o7.js → surrealql-DcFIgIgS.js} +2 -2
  68. package/dist/client/assets/{svelte-BogBuioE.js → svelte-5uir_iV_.js} +2 -2
  69. package/dist/client/assets/{templ-C4v7sOsC.js → templ-IHhNUhu5.js} +2 -2
  70. package/dist/client/assets/{tex-KXY3LBOg.js → tex-DbjEQBr6.js} +2 -2
  71. package/dist/client/assets/textarea-C6kz4NLj.js +46 -0
  72. package/dist/client/assets/{ts-tags-BUzdov0d.js → ts-tags-xML8Swir.js} +2 -2
  73. package/dist/client/assets/{tsx-DCEZNbvU.js → tsx-BwMUW3gO.js} +2 -2
  74. package/dist/client/assets/{twig-lX5bLK2U.js → twig-BRlCEyLn.js} +2 -2
  75. package/dist/client/assets/{typescript-D0XyCzdv.js → typescript-DC1qlCSF.js} +2 -2
  76. package/dist/client/assets/{vue-BVUKhxXR.js → vue-Cta_ijHF.js} +2 -2
  77. package/dist/client/assets/{vue-html-BFZ9Hu9Q.js → vue-html-C6cjjUjR.js} +2 -2
  78. package/dist/client/assets/{vue-vine-DwK9p9S2.js → vue-vine-DMiSqDlT.js} +2 -2
  79. package/dist/client/assets/{xml-BZnktucc.js → xml-DJpzo_Kg.js} +2 -2
  80. package/dist/client/assets/{xsl-BQvXVv3K.js → xsl-DAqFroi4.js} +2 -2
  81. package/dist/client/assets/{yaml-CjmvH72q.js → yaml-Brab7tCG.js} +2 -2
  82. package/dist/client/index.html +3 -3
  83. package/package.json +1 -1
  84. package/dist/client/assets/PreviewPanel-DtVNEw5j.js +0 -2
  85. package/dist/client/assets/arrow-right-BG7cUHWK.js +0 -2
  86. package/dist/client/assets/index-C0FwegCm.css +0 -1
  87. package/dist/client/assets/index-CNTBEOVF.js +0 -640
  88. package/dist/client/assets/textarea-DHdA8g53.js +0 -46
package/dist/bin.mjs CHANGED
@@ -55,6 +55,7 @@ import * as Exit from "effect/Exit";
55
55
  import * as SchemaGetter from "effect/SchemaGetter";
56
56
  import * as Config from "effect/Config";
57
57
  import * as NodeReadline from "node:readline";
58
+ import * as Clock from "effect/Clock";
58
59
  import * as Cache from "effect/Cache";
59
60
  import * as Deferred from "effect/Deferred";
60
61
  import * as Equal from "effect/Equal";
@@ -62,7 +63,6 @@ import * as Semaphore from "effect/Semaphore";
62
63
  import * as Scope from "effect/Scope";
63
64
  import { parse } from "yaml";
64
65
  import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
65
- import * as Clock from "effect/Clock";
66
66
  import * as Metric from "effect/Metric";
67
67
  import * as Queue from "effect/Queue";
68
68
  import { cast, dual } from "effect/Function";
@@ -121,7 +121,7 @@ const closeServer = (server) => {
121
121
  * NetService - Service tag for startup networking helpers.
122
122
  */
123
123
  var NetService = class extends Context.Service()("@p4code/shared/Net/NetService") {};
124
- const make$71 = () => {
124
+ const make$72 = () => {
125
125
  /**
126
126
  * Returns true when a TCP server can bind to {host, port}.
127
127
  * `EADDRNOTAVAIL` is treated as available so IPv6-absent hosts don't fail
@@ -230,10 +230,10 @@ const make$71 = () => {
230
230
  })
231
231
  };
232
232
  };
233
- const layer$67 = Layer.sync(NetService, make$71);
233
+ const layer$68 = Layer.sync(NetService, make$72);
234
234
  //#endregion
235
235
  //#region package.json
236
- var version = "0.0.49";
236
+ var version = "0.0.51";
237
237
  //#endregion
238
238
  //#region src/config.ts
239
239
  /**
@@ -254,8 +254,8 @@ var ServerConfig$1 = class extends Context.Service()("@p4code/cli/config/ServerC
254
254
  /** @deprecated Import and use `layerTest` from this module. */
255
255
  static layerTest = (cwd, baseDirOrPrefix) => layerTest$2(cwd, baseDirOrPrefix);
256
256
  };
257
- const make$70 = (config) => ServerConfig$1.of(config);
258
- const layer$66 = (config) => Layer.succeed(ServerConfig$1, make$70(config));
257
+ const make$71 = (config) => ServerConfig$1.of(config);
258
+ const layer$67 = (config) => Layer.succeed(ServerConfig$1, make$71(config));
259
259
  const deriveServerPaths = Effect.fn(function* (baseDir, devUrl, options = {}) {
260
260
  const { join } = yield* Path.Path;
261
261
  const stateDir = join(baseDir, devUrl !== void 0 && !options.baseDirIsExplicit ? "dev" : "userdata");
@@ -869,7 +869,7 @@ function compareSemverVersions(left, right) {
869
869
  * timeout, every failure swallowed, and never anything that changes an exit
870
870
  * code. A version check that can break a command is worse than no check.
871
871
  */
872
- const PACKAGE_NAME = "@p4code/cli";
872
+ const PACKAGE_NAME$1 = "@p4code/cli";
873
873
  const CHECK_INTERVAL = Duration.hours(24);
874
874
  const REQUEST_TIMEOUT = Duration.seconds(3);
875
875
  /**
@@ -889,7 +889,7 @@ const encodeCacheEntry = Schema$1.encodeEffect(CacheEntryJson);
889
889
  const NpmLatest = Schema$1.Struct({ version: Schema$1.optional(Schema$1.String) });
890
890
  const fetchLatestVersion = Effect.fn("cli.updateCheck.fetch")(function* () {
891
891
  const client = yield* HttpClient.HttpClient;
892
- const request = HttpClientRequest.get(`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`).pipe(HttpClientRequest.setHeader("accept", "application/json"));
892
+ const request = HttpClientRequest.get(`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME$1)}/latest`).pipe(HttpClientRequest.setHeader("accept", "application/json"));
893
893
  const response = yield* client.execute(request).pipe(Effect.timeout(REQUEST_TIMEOUT));
894
894
  if (response.status < 200 || response.status >= 300) return null;
895
895
  const version = (yield* response.json.pipe(Effect.flatMap(Schema$1.decodeUnknownEffect(NpmLatest)))).version?.trim();
@@ -923,7 +923,7 @@ const resolveLatestVersion = Effect.fn("cli.updateCheck.resolve")(function* (sta
923
923
  * `service update` restarts it onto the new one.
924
924
  */
925
925
  function formatUpdateReminder(input) {
926
- return [`Update available: ${PACKAGE_NAME}@${input.latestVersion} (running ${input.currentVersion}).`, ` npm i -g ${PACKAGE_NAME}@latest && p4c service update`].join("\n");
926
+ return [`Update available: ${PACKAGE_NAME$1}@${input.latestVersion} (running ${input.currentVersion}).`, ` npm i -g ${PACKAGE_NAME$1}@latest && p4c service update`].join("\n");
927
927
  }
928
928
  /**
929
929
  * The reminder line, or null when there is nothing to say. Callers print it;
@@ -4819,6 +4819,87 @@ const ThreadMetadataUpdatedPayload = Schema$1.Struct({
4819
4819
  name: Schema$1.optional(TrimmedNonEmptyStringSchema$1),
4820
4820
  metadata: Schema$1.optional(UnknownRecordSchema)
4821
4821
  });
4822
+ /**
4823
+ * Per-source attribution for what is occupying the context window, as reported
4824
+ * by a provider that can answer the question. Only the Claude adapter fills this
4825
+ * in today; every field is optional so a provider can report the parts it knows.
4826
+ *
4827
+ * `isDeferred` / `isLoaded` are the interesting half. Providers defer most tool
4828
+ * definitions behind tool search, so a deferred entry's tokens are what it
4829
+ * *would* cost rather than what it costs right now, and presenting the two as
4830
+ * one number overstates usage.
4831
+ */
4832
+ const ContextWindowNamedCost = Schema$1.Struct({
4833
+ name: TrimmedNonEmptyStringSchema$1,
4834
+ tokens: NonNegativeInt
4835
+ });
4836
+ const ContextWindowDeferrableTool = Schema$1.Struct({
4837
+ name: TrimmedNonEmptyStringSchema$1,
4838
+ tokens: NonNegativeInt,
4839
+ isLoaded: Schema$1.optional(Schema$1.Boolean)
4840
+ });
4841
+ const ContextWindowBreakdown = Schema$1.Struct({
4842
+ /** The model the breakdown was measured against, which is not always the thread's current selection. */
4843
+ model: Schema$1.optional(TrimmedNonEmptyStringSchema$1),
4844
+ /** Top-level buckets the provider itself divides the window into. */
4845
+ categories: Schema$1.optional(Schema$1.Array(Schema$1.Struct({
4846
+ name: TrimmedNonEmptyStringSchema$1,
4847
+ tokens: NonNegativeInt,
4848
+ isDeferred: Schema$1.optional(Schema$1.Boolean)
4849
+ }))),
4850
+ mcpTools: Schema$1.optional(Schema$1.Array(Schema$1.Struct({
4851
+ name: TrimmedNonEmptyStringSchema$1,
4852
+ serverName: TrimmedNonEmptyStringSchema$1,
4853
+ tokens: NonNegativeInt,
4854
+ isLoaded: Schema$1.optional(Schema$1.Boolean)
4855
+ }))),
4856
+ skills: Schema$1.optional(Schema$1.Struct({
4857
+ totalSkills: NonNegativeInt,
4858
+ includedSkills: NonNegativeInt,
4859
+ tokens: NonNegativeInt,
4860
+ entries: Schema$1.optional(Schema$1.Array(Schema$1.Struct({
4861
+ name: TrimmedNonEmptyStringSchema$1,
4862
+ source: Schema$1.optional(TrimmedNonEmptyStringSchema$1),
4863
+ tokens: NonNegativeInt
4864
+ })))
4865
+ })),
4866
+ memoryFiles: Schema$1.optional(Schema$1.Array(Schema$1.Struct({
4867
+ path: TrimmedNonEmptyStringSchema$1,
4868
+ type: Schema$1.optional(TrimmedNonEmptyStringSchema$1),
4869
+ tokens: NonNegativeInt
4870
+ }))),
4871
+ agents: Schema$1.optional(Schema$1.Array(Schema$1.Struct({
4872
+ agentType: TrimmedNonEmptyStringSchema$1,
4873
+ source: Schema$1.optional(TrimmedNonEmptyStringSchema$1),
4874
+ tokens: NonNegativeInt
4875
+ }))),
4876
+ systemTools: Schema$1.optional(Schema$1.Array(ContextWindowNamedCost)),
4877
+ deferredBuiltinTools: Schema$1.optional(Schema$1.Array(ContextWindowDeferrableTool)),
4878
+ systemPromptSections: Schema$1.optional(Schema$1.Array(ContextWindowNamedCost)),
4879
+ slashCommands: Schema$1.optional(Schema$1.Struct({
4880
+ totalCommands: NonNegativeInt,
4881
+ includedCommands: NonNegativeInt,
4882
+ tokens: NonNegativeInt
4883
+ })),
4884
+ /** How the conversation itself divides up, as opposed to the prompt scaffolding above. */
4885
+ messages: Schema$1.optional(Schema$1.Struct({
4886
+ toolCallTokens: NonNegativeInt,
4887
+ toolResultTokens: NonNegativeInt,
4888
+ attachmentTokens: NonNegativeInt,
4889
+ assistantMessageTokens: NonNegativeInt,
4890
+ userMessageTokens: NonNegativeInt,
4891
+ redirectedContextTokens: NonNegativeInt,
4892
+ unattributedTokens: NonNegativeInt,
4893
+ toolCallsByType: Schema$1.optional(Schema$1.Array(Schema$1.Struct({
4894
+ name: TrimmedNonEmptyStringSchema$1,
4895
+ callTokens: NonNegativeInt,
4896
+ resultTokens: NonNegativeInt
4897
+ }))),
4898
+ attachmentsByType: Schema$1.optional(Schema$1.Array(ContextWindowNamedCost))
4899
+ })),
4900
+ /** Token count at which the provider compacts on its own, when it does. */
4901
+ autoCompactThreshold: Schema$1.optional(NonNegativeInt)
4902
+ });
4822
4903
  const ThreadTokenUsageSnapshot = Schema$1.Struct({
4823
4904
  usedTokens: NonNegativeInt,
4824
4905
  totalProcessedTokens: Schema$1.optional(NonNegativeInt),
@@ -4834,7 +4915,8 @@ const ThreadTokenUsageSnapshot = Schema$1.Struct({
4834
4915
  lastReasoningOutputTokens: Schema$1.optional(NonNegativeInt),
4835
4916
  toolUses: Schema$1.optional(NonNegativeInt),
4836
4917
  durationMs: Schema$1.optional(NonNegativeInt),
4837
- compactsAutomatically: Schema$1.optional(Schema$1.Boolean)
4918
+ compactsAutomatically: Schema$1.optional(Schema$1.Boolean),
4919
+ breakdown: Schema$1.optional(ContextWindowBreakdown)
4838
4920
  });
4839
4921
  const ThreadTokenUsageUpdatedPayload = Schema$1.Struct({ usage: ThreadTokenUsageSnapshot });
4840
4922
  const ThreadRealtimeStartedPayload = Schema$1.Struct({ realtimeSessionId: Schema$1.optional(TrimmedNonEmptyStringSchema$1) });
@@ -7235,6 +7317,59 @@ const TaskStreamEvent = Schema$1.Union([
7235
7317
  taskId: TaskId
7236
7318
  })
7237
7319
  ]);
7320
+ /**
7321
+ * Start an agent on a task.
7322
+ *
7323
+ * The input is only the task, deliberately. Everything else the thread needs —
7324
+ * which project, which model, what prompt — is resolved server-side from the
7325
+ * task's own project, because the caller that matters next is not a person
7326
+ * clicking a button but Phase 12's orchestrator, and a client that supplies the
7327
+ * model is a client that can start a task on a provider the user does not use.
7328
+ *
7329
+ * Worktrees are absent for a harder reason: `prepareWorktree` needs a base
7330
+ * branch, a task carries none, and nothing resolves one server-side. A thread
7331
+ * started from a task therefore runs in the project's own checkout.
7332
+ */
7333
+ const TaskStartThreadInput = Schema$1.Struct({ taskId: TaskIdField });
7334
+ /**
7335
+ * The started thread and the task as it now stands.
7336
+ *
7337
+ * Both are returned because both changed: the caller navigates to the thread,
7338
+ * and the board would otherwise show the old status until the change event
7339
+ * arrived.
7340
+ */
7341
+ const TaskStartThreadResult = Schema$1.Struct({
7342
+ task: Task,
7343
+ threadId: ThreadId
7344
+ });
7345
+ /**
7346
+ * A task cannot be started, and the reason is a property of the task or the
7347
+ * server rather than a failure.
7348
+ *
7349
+ * Each reason is a different thing for the user to fix, which is why this is a
7350
+ * union and not one message. `no-model` is the one worth explaining: a task
7351
+ * carries no model selection, so the thread runs on the project's default. The
7352
+ * server-side fallback that exists for first-run bootstrap is deliberately not
7353
+ * used here — it hardcodes a provider, so reaching for it would silently start
7354
+ * someone who works in Claude on Codex. Failing is the honest answer.
7355
+ */
7356
+ const TaskNotStartableReason = Schema$1.Literals([
7357
+ "no-project",
7358
+ "project-missing",
7359
+ "no-model"
7360
+ ]);
7361
+ var TaskNotStartableError = class extends Schema$1.TaggedErrorClass()("TaskNotStartableError", {
7362
+ taskId: TaskId,
7363
+ reason: TaskNotStartableReason
7364
+ }) {
7365
+ get message() {
7366
+ switch (this.reason) {
7367
+ case "no-project": return "This task has no project, so there is nowhere to run it. Set a project on the task first.";
7368
+ case "project-missing": return "This task's project is not registered on this server. Point it at a project that is.";
7369
+ case "no-model": return "This task's project has no default model. Choose one in the project's settings, then start the task again.";
7370
+ }
7371
+ }
7372
+ };
7238
7373
  /** Result shape for the board's list RPC. */
7239
7374
  const TaskListResult$1 = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
7240
7375
  /** Result shape for every RPC that resolves to a single task. */
@@ -8004,6 +8139,7 @@ const WS_METHODS = {
8004
8139
  tasksCreate: "tasks.create",
8005
8140
  tasksUpdate: "tasks.update",
8006
8141
  tasksDelete: "tasks.delete",
8142
+ tasksStartThread: "tasks.startThread",
8007
8143
  hubGetSyncStatus: "hub.getSyncStatus",
8008
8144
  hubConnect: "hub.connect",
8009
8145
  hubDisconnect: "hub.disconnect",
@@ -8078,6 +8214,30 @@ const WsTasksDeleteRpc = Rpc.make(WS_METHODS.tasksDelete, {
8078
8214
  error: TaskRpcError
8079
8215
  });
8080
8216
  /**
8217
+ * Start an agent on a task.
8218
+ *
8219
+ * This is the one task RPC that is not CRUD: it creates a thread, seeds it from
8220
+ * the task, and links the two. It lives in the `tasks.*` namespace because the
8221
+ * task is what it reads and writes — the thread is the side effect, not the
8222
+ * subject — and because the caller always names a task and never a thread.
8223
+ *
8224
+ * The error union is wider than the other task methods for the same reason. A
8225
+ * start can fail because the task is not startable, which is a property of the
8226
+ * row, or because the turn could not be dispatched, which is the orchestration
8227
+ * engine's own failure and already has a type.
8228
+ */
8229
+ const WsTasksStartThreadRpc = Rpc.make(WS_METHODS.tasksStartThread, {
8230
+ payload: TaskStartThreadInput,
8231
+ success: TaskStartThreadResult,
8232
+ error: Schema$1.Union([
8233
+ TaskNotFoundError,
8234
+ TaskNotStartableError,
8235
+ TaskStoreError,
8236
+ OrchestrationDispatchCommandError,
8237
+ EnvironmentAuthorizationError
8238
+ ])
8239
+ });
8240
+ /**
8081
8241
  * Hub link and skill sync.
8082
8242
  *
8083
8243
  * Every one of these answers with the whole status rather than an
@@ -8590,7 +8750,7 @@ const WsSubscribeAuthAccessRpc = Rpc.make(WS_METHODS.subscribeAuthAccess, {
8590
8750
  error: Schema$1.Union([AuthAccessStreamError, EnvironmentAuthorizationError]),
8591
8751
  stream: true
8592
8752
  });
8593
- const WsRpcGroup = RpcGroup.make(WsServerProbeRpc, WsServerGetConfigRpc, WsServerRefreshProvidersRpc, WsServerUpdateProviderRpc, WsServerUpdateServerRpc, WsServerUpsertKeybindingRpc, WsServerRemoveKeybindingRpc, WsServerGetSettingsRpc, WsServerUpdateSettingsRpc, WsServerDiscoverSourceControlRpc, WsServerGetTraceDiagnosticsRpc, WsServerGetProcessDiagnosticsRpc, WsServerGetProcessResourceHistoryRpc, WsServerSignalProcessRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsSourceControlPublishRepositoryRpc, WsProjectsListEntriesRpc, WsProjectsReadFileRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsShellOpenInEditorRpc, WsFilesystemBrowseRpc, WsAssetsCreateUrlRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsVcsListRefsRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsReviewGetDiffPreviewRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsPreviewOpenRpc, WsPreviewNavigateRpc, WsPreviewResizeRpc, WsPreviewRefreshRpc, WsPreviewCloseRpc, WsPreviewListRpc, WsPreviewReportStatusRpc, WsPreviewAutomationConnectRpc, WsPreviewAutomationRespondRpc, WsPreviewAutomationFocusHostRpc, WsSubscribePreviewEventsRpc, WsSubscribeDiscoveredLocalServersRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsTasksListRpc, WsTasksGetRpc, WsTasksCreateRpc, WsTasksUpdateRpc, WsTasksDeleteRpc, WsSubscribeTasksRpc, WsHubGetSyncStatusRpc, WsHubConnectRpc, WsHubDisconnectRpc, WsHubSetSyncModeRpc, WsHubSetShareModeRpc, WsHubMintTokenRpc, WsSkillsSyncRpc, WsSkillsPublishRpc, WsSkillsPublishAllRpc, WsSkillsUnpublishRpc, WsAssetsReadRpc, WsAssetsSaveRpc, WsAssetsDeleteRpc, WsMcpListRpc, WsMcpSaveRpc, WsMcpRemoveRpc, WsMcpSetSecretRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc);
8753
+ const WsRpcGroup = RpcGroup.make(WsServerProbeRpc, WsServerGetConfigRpc, WsServerRefreshProvidersRpc, WsServerUpdateProviderRpc, WsServerUpdateServerRpc, WsServerUpsertKeybindingRpc, WsServerRemoveKeybindingRpc, WsServerGetSettingsRpc, WsServerUpdateSettingsRpc, WsServerDiscoverSourceControlRpc, WsServerGetTraceDiagnosticsRpc, WsServerGetProcessDiagnosticsRpc, WsServerGetProcessResourceHistoryRpc, WsServerSignalProcessRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsSourceControlPublishRepositoryRpc, WsProjectsListEntriesRpc, WsProjectsReadFileRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsShellOpenInEditorRpc, WsFilesystemBrowseRpc, WsAssetsCreateUrlRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsVcsListRefsRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsReviewGetDiffPreviewRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsPreviewOpenRpc, WsPreviewNavigateRpc, WsPreviewResizeRpc, WsPreviewRefreshRpc, WsPreviewCloseRpc, WsPreviewListRpc, WsPreviewReportStatusRpc, WsPreviewAutomationConnectRpc, WsPreviewAutomationRespondRpc, WsPreviewAutomationFocusHostRpc, WsSubscribePreviewEventsRpc, WsSubscribeDiscoveredLocalServersRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsTasksListRpc, WsTasksGetRpc, WsTasksCreateRpc, WsTasksUpdateRpc, WsTasksDeleteRpc, WsTasksStartThreadRpc, WsSubscribeTasksRpc, WsHubGetSyncStatusRpc, WsHubConnectRpc, WsHubDisconnectRpc, WsHubSetSyncModeRpc, WsHubSetShareModeRpc, WsHubMintTokenRpc, WsSkillsSyncRpc, WsSkillsPublishRpc, WsSkillsPublishAllRpc, WsSkillsUnpublishRpc, WsAssetsReadRpc, WsAssetsSaveRpc, WsAssetsDeleteRpc, WsMcpListRpc, WsMcpSaveRpc, WsMcpRemoveRpc, WsMcpSetSecretRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc);
8594
8754
  //#endregion
8595
8755
  //#region ../../packages/shared/src/oauthScope.ts
8596
8756
  const OAUTH_SCOPE_TOKEN = /^[\u0021\u0023-\u005b\u005d-\u007e]+$/u;
@@ -8736,7 +8896,7 @@ function deriveAuthClientMetadata(input) {
8736
8896
  //#endregion
8737
8897
  //#region src/auth/EnvironmentAuthPolicy.ts
8738
8898
  var EnvironmentAuthPolicy = class extends Context.Service()("@p4code/cli/auth/EnvironmentAuthPolicy") {};
8739
- const make$69 = Effect.gen(function* () {
8899
+ const make$70 = Effect.gen(function* () {
8740
8900
  const config = yield* ServerConfig$1;
8741
8901
  const isRemoteReachable = isRemoteReachableHost(config.host);
8742
8902
  const policy = config.mode === "desktop" ? isRemoteReachable ? "remote-reachable" : "desktop-managed-local" : isRemoteReachable ? "remote-reachable" : "loopback-browser";
@@ -8754,7 +8914,7 @@ const make$69 = Effect.gen(function* () {
8754
8914
  };
8755
8915
  return EnvironmentAuthPolicy.of({ getDescriptor: () => Effect.succeed(descriptor).pipe(Effect.withSpan("EnvironmentAuthPolicy.getDescriptor")) });
8756
8916
  });
8757
- const layer$65 = Layer.effect(EnvironmentAuthPolicy, make$69);
8917
+ const layer$66 = Layer.effect(EnvironmentAuthPolicy, make$70);
8758
8918
  //#endregion
8759
8919
  //#region src/persistence/Errors.ts
8760
8920
  function summarizeSchemaIssue(issue) {
@@ -8935,7 +9095,7 @@ function toPersistenceSqlOrDecodeError$6(sqlOperation, decodeOperation, correlat
8935
9095
  cause
8936
9096
  });
8937
9097
  }
8938
- const make$68 = Effect.gen(function* () {
9098
+ const make$69 = Effect.gen(function* () {
8939
9099
  const sql = yield* SqlClient.SqlClient;
8940
9100
  const createSessionRow = SqlSchema.void({
8941
9101
  Request: CreateAuthSessionInput,
@@ -9069,7 +9229,7 @@ const make$68 = Effect.gen(function* () {
9069
9229
  setLastConnectedAt
9070
9230
  };
9071
9231
  });
9072
- const layer$64 = Layer.effect(AuthSessionRepository, make$68);
9232
+ const layer$65 = Layer.effect(AuthSessionRepository, make$69);
9073
9233
  //#endregion
9074
9234
  //#region src/auth/ServerSecretStore.ts
9075
9235
  const secretStoreErrorContext = {
@@ -9136,7 +9296,7 @@ const isSecretStoreError = Schema$1.is(SecretStoreError);
9136
9296
  const isPlatformError = (value) => Predicate.isTagged(value, "PlatformError");
9137
9297
  const isSecretAlreadyExistsError = (error) => "cause" in error && isPlatformError(error.cause) && error.cause.reason._tag === "AlreadyExists";
9138
9298
  var ServerSecretStore = class extends Context.Service()("@p4code/cli/auth/ServerSecretStore") {};
9139
- const make$67 = Effect.gen(function* () {
9299
+ const make$68 = Effect.gen(function* () {
9140
9300
  const crypto = yield* Crypto.Crypto;
9141
9301
  const fileSystem = yield* FileSystem.FileSystem;
9142
9302
  const path = yield* Path.Path;
@@ -9206,7 +9366,7 @@ const make$67 = Effect.gen(function* () {
9206
9366
  remove
9207
9367
  });
9208
9368
  });
9209
- const layer$63 = Layer.effect(ServerSecretStore, make$67);
9369
+ const layer$64 = Layer.effect(ServerSecretStore, make$68);
9210
9370
  //#endregion
9211
9371
  //#region src/auth/SessionStore.ts
9212
9372
  var MalformedSessionTokenError = class extends Schema$1.TaggedErrorClass()("MalformedSessionTokenError", {}) {
@@ -9444,7 +9604,7 @@ function toAuthClientSession(input) {
9444
9604
  current: false
9445
9605
  };
9446
9606
  }
9447
- const make$66 = Effect.gen(function* () {
9607
+ const make$67 = Effect.gen(function* () {
9448
9608
  const crypto = yield* Crypto.Crypto;
9449
9609
  const serverConfig = yield* ServerConfig$1;
9450
9610
  const secretStore = yield* ServerSecretStore;
@@ -9758,7 +9918,7 @@ const make$66 = Effect.gen(function* () {
9758
9918
  markDisconnected
9759
9919
  });
9760
9920
  });
9761
- const layer$62 = Layer.effect(SessionStore, make$66).pipe(Layer.provideMerge(layer$64));
9921
+ const layer$63 = Layer.effect(SessionStore, make$67).pipe(Layer.provideMerge(layer$65));
9762
9922
  //#endregion
9763
9923
  //#region src/persistence/AuthPairingLinks.ts
9764
9924
  const AuthPairingLinkRecord = Schema$1.Struct({
@@ -9819,7 +9979,7 @@ function toPersistenceSqlOrDecodeError$5(sqlOperation, decodeOperation, correlat
9819
9979
  cause
9820
9980
  });
9821
9981
  }
9822
- const make$65 = Effect.gen(function* () {
9982
+ const make$66 = Effect.gen(function* () {
9823
9983
  const sql = yield* SqlClient.SqlClient;
9824
9984
  const createPairingLinkRow = SqlSchema.void({
9825
9985
  Request: CreateAuthPairingLinkInput,
@@ -9954,7 +10114,7 @@ const make$65 = Effect.gen(function* () {
9954
10114
  getByCredential
9955
10115
  };
9956
10116
  });
9957
- const layer$61 = Layer.effect(AuthPairingLinkRepository, make$65);
10117
+ const layer$62 = Layer.effect(AuthPairingLinkRepository, make$66);
9958
10118
  //#endregion
9959
10119
  //#region src/auth/PairingGrantStore.ts
9960
10120
  var UnknownBootstrapCredentialError = class extends Schema$1.TaggedErrorClass()("UnknownBootstrapCredentialError", {}) {
@@ -10049,7 +10209,7 @@ const DEV_STARTUP_TTL_HOURS = Duration.hours(24);
10049
10209
  const PAIRING_TOKEN_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
10050
10210
  const PAIRING_TOKEN_LENGTH = 12;
10051
10211
  const PAIRING_TOKEN_REJECTION_LIMIT = Math.floor(256 / 32) * 32;
10052
- const make$64 = Effect.gen(function* () {
10212
+ const make$65 = Effect.gen(function* () {
10053
10213
  const crypto = yield* Crypto.Crypto;
10054
10214
  const config = yield* ServerConfig$1;
10055
10215
  const pairingLinks = yield* AuthPairingLinkRepository;
@@ -10247,7 +10407,7 @@ const make$64 = Effect.gen(function* () {
10247
10407
  consume
10248
10408
  });
10249
10409
  });
10250
- const layer$60 = Layer.effect(PairingGrantStore, make$64).pipe(Layer.provideMerge(layer$61));
10410
+ const layer$61 = Layer.effect(PairingGrantStore, make$65).pipe(Layer.provideMerge(layer$62));
10251
10411
  //#endregion
10252
10412
  //#region src/persistence/Migrations/001_OrchestrationEvents.ts
10253
10413
  var _001_OrchestrationEvents_default = Effect.gen(function* () {
@@ -12020,7 +12180,7 @@ function parseBearerToken(request) {
12020
12180
  const token = header.slice(7).trim();
12021
12181
  return token.length > 0 ? token : null;
12022
12182
  }
12023
- const make$63 = Effect.gen(function* () {
12183
+ const make$64 = Effect.gen(function* () {
12024
12184
  const policy = yield* EnvironmentAuthPolicy;
12025
12185
  const bootstrapCredentials = yield* PairingGrantStore;
12026
12186
  const sessions = yield* SessionStore;
@@ -12215,9 +12375,9 @@ const make$63 = Effect.gen(function* () {
12215
12375
  issueStartupPairingUrl
12216
12376
  });
12217
12377
  });
12218
- const layer$59 = Layer.effect(EnvironmentAuth, make$63).pipe(Layer.provideMerge(layer$60), Layer.provideMerge(layer$62), Layer.provideMerge(layer$65));
12219
- const storageLayer = Layer.mergeAll(layer$63, layerConfig);
12220
- const runtimeLayer = layer$59.pipe(Layer.provideMerge(storageLayer));
12378
+ const layer$60 = Layer.effect(EnvironmentAuth, make$64).pipe(Layer.provideMerge(layer$61), Layer.provideMerge(layer$63), Layer.provideMerge(layer$66));
12379
+ const storageLayer = Layer.mergeAll(layer$64, layerConfig);
12380
+ const runtimeLayer = layer$60.pipe(Layer.provideMerge(storageLayer));
12221
12381
  //#endregion
12222
12382
  //#region src/cliAuthFormat.ts
12223
12383
  const newline = "\n";
@@ -12972,7 +13132,7 @@ const runWithEnvironmentAuth = (flags, run, options) => Effect.gen(function* ()
12972
13132
  }
12973
13133
  return yield* Effect.gen(function* () {
12974
13134
  return yield* run(yield* EnvironmentAuth);
12975
- }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer).pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
13135
+ }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer).pipe(Layer.provide(layer$67(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
12976
13136
  });
12977
13137
  const ttlFlag = Flag.string("ttl").pipe(Flag.withSchema(DurationFromString), Flag.withDescription("TTL, for example `5m`, `1h`, `30d`, or `15 minutes`."), Flag.optional);
12978
13138
  const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Emit JSON instead of human-readable output."), Flag.withDefault(false));
@@ -13069,6 +13229,64 @@ const authCommand = Command.make("auth").pipe(Command.withDescription("Manage th
13069
13229
  * @module HubConfig
13070
13230
  */
13071
13231
  var HubConfig = class extends Context.Service()("@p4code/cli/hub/HubConfig") {};
13232
+ const FAILURE_WINDOW_MS = 6e4;
13233
+ const BLOCK_DURATION_MS = 6e4;
13234
+ const initialThrottleState = {
13235
+ windowStartMillis: 0,
13236
+ failures: 0,
13237
+ blockedUntilMillis: 0
13238
+ };
13239
+ /** Whether a *failing* request should be refused outright rather than checked. */
13240
+ const isBlocked = (state, nowMillis) => nowMillis < state.blockedUntilMillis;
13241
+ /** Seconds a caller should wait, for `retry-after`. Always at least one. */
13242
+ const retryAfterSeconds = (state, nowMillis) => Math.max(1, Math.ceil((state.blockedUntilMillis - nowMillis) / 1e3));
13243
+ /**
13244
+ * Count a failure, and start blocking once the window fills.
13245
+ *
13246
+ * The window is fixed rather than sliding: a sliding window needs the timestamp
13247
+ * of every attempt, which is unbounded memory for an unbounded attacker. A
13248
+ * fixed window lets a burst straddle a boundary and land up to twice the limit,
13249
+ * which for this purpose is a rounding error against the size of the search
13250
+ * space.
13251
+ */
13252
+ const recordFailure = (state, nowMillis, limits = {
13253
+ maxFailures: 10,
13254
+ windowMillis: FAILURE_WINDOW_MS,
13255
+ blockMillis: BLOCK_DURATION_MS
13256
+ }) => {
13257
+ if (nowMillis - state.windowStartMillis >= limits.windowMillis) return {
13258
+ windowStartMillis: nowMillis,
13259
+ failures: 1,
13260
+ blockedUntilMillis: 0
13261
+ };
13262
+ const failures = state.failures + 1;
13263
+ return {
13264
+ windowStartMillis: state.windowStartMillis,
13265
+ failures,
13266
+ blockedUntilMillis: failures >= limits.maxFailures ? nowMillis + limits.blockMillis : state.blockedUntilMillis
13267
+ };
13268
+ };
13269
+ var HubAuthThrottle = class extends Context.Service()("@p4code/cli/hub/HubAuthThrottle") {};
13270
+ const DEFAULT_LIMITS = {
13271
+ maxFailures: 10,
13272
+ windowMillis: FAILURE_WINDOW_MS,
13273
+ blockMillis: BLOCK_DURATION_MS
13274
+ };
13275
+ const make$63 = Effect.fn("HubAuthThrottle.make")(function* (limits = DEFAULT_LIMITS) {
13276
+ const state = yield* Ref.make(initialThrottleState);
13277
+ return HubAuthThrottle.of({
13278
+ shouldRefuse: Effect.gen(function* () {
13279
+ const now = yield* Clock.currentTimeMillis;
13280
+ return isBlocked(yield* Ref.get(state), now);
13281
+ }),
13282
+ recordFailure: Effect.gen(function* () {
13283
+ const now = yield* Clock.currentTimeMillis;
13284
+ const next = yield* Ref.updateAndGet(state, (current) => recordFailure(current, now, limits));
13285
+ return retryAfterSeconds(next, now);
13286
+ })
13287
+ });
13288
+ });
13289
+ const layer$59 = Layer.effect(HubAuthThrottle, make$63());
13072
13290
  //#endregion
13073
13291
  //#region src/hub/HubAuth.ts
13074
13292
  /**
@@ -13127,6 +13345,31 @@ var HubUnauthorizedError = class {
13127
13345
  _tag = "HubUnauthorizedError";
13128
13346
  };
13129
13347
  /**
13348
+ * Too many failed attempts recently; this one was not even checked.
13349
+ *
13350
+ * Distinct from `HubUnauthorizedError` so the response can differ. A 429 is the
13351
+ * honest answer — the credential was not evaluated, so calling it invalid would
13352
+ * be a claim the hub did not make — and it tells a legitimate server retrying a
13353
+ * stale token to back off rather than hammer.
13354
+ */
13355
+ var HubThrottledError = class {
13356
+ _tag = "HubThrottledError";
13357
+ retryAfterSeconds;
13358
+ constructor(retryAfterSeconds) {
13359
+ this.retryAfterSeconds = retryAfterSeconds;
13360
+ }
13361
+ };
13362
+ const hubThrottledResponse = (retryAfterSeconds) => HttpServerResponse.jsonUnsafe({
13363
+ error: "too_many_hub_credential_failures",
13364
+ message: "Too many failed credential attempts. Try again shortly."
13365
+ }, {
13366
+ status: 429,
13367
+ headers: {
13368
+ "cache-control": "no-store",
13369
+ "retry-after": String(retryAfterSeconds)
13370
+ }
13371
+ });
13372
+ /**
13130
13373
  * Fails with {@link HubUnauthorizedError} unless the request carries a
13131
13374
  * configured bearer token. Route handlers run this first and let
13132
13375
  * `respondToHubAuthFailure` turn the failure into a 401.
@@ -13134,11 +13377,17 @@ var HubUnauthorizedError = class {
13134
13377
  const authenticateHubRequest = Effect.fn("HubAuth.authenticateHubRequest")(function* () {
13135
13378
  const request = yield* HttpServerRequest.HttpServerRequest;
13136
13379
  const { tokenHashes } = yield* HubConfig;
13380
+ const throttle = yield* HubAuthThrottle;
13137
13381
  const token = readBearerToken(request.headers.authorization);
13138
- if (!isAuthorizedHubToken(token, tokenHashes)) {
13139
- yield* Effect.logWarning("rejected hub request with an unusable credential", { reason: token.length === 0 ? "missing_bearer_token" : "unknown_token" });
13140
- return yield* Effect.fail(new HubUnauthorizedError());
13382
+ if (isAuthorizedHubToken(token, tokenHashes)) return;
13383
+ if (yield* throttle.shouldRefuse) {
13384
+ const retryAfter = yield* throttle.recordFailure;
13385
+ yield* Effect.logWarning("refused hub request without checking; too many recent failures", { reason: token.length === 0 ? "missing_bearer_token" : "unknown_token" });
13386
+ return yield* Effect.fail(new HubThrottledError(retryAfter));
13141
13387
  }
13388
+ yield* throttle.recordFailure;
13389
+ yield* Effect.logWarning("rejected hub request with an unusable credential", { reason: token.length === 0 ? "missing_bearer_token" : "unknown_token" });
13390
+ return yield* Effect.fail(new HubUnauthorizedError());
13142
13391
  });
13143
13392
  const SHA256_HEX = /^[0-9a-f]{64}$/;
13144
13393
  /**
@@ -13547,6 +13796,7 @@ const storeUnavailable = HttpServerResponse.jsonUnsafe({
13547
13796
  */
13548
13797
  const respondToHubFailures = (handler) => handler.pipe(Effect.catch((cause) => {
13549
13798
  if (cause._tag === "HubUnauthorizedError") return Effect.succeed(hubUnauthorizedResponse);
13799
+ if (cause._tag === "HubThrottledError") return Effect.succeed(hubThrottledResponse(cause.retryAfterSeconds));
13550
13800
  if (cause._tag === "InvalidHubRequest") return Effect.succeed(badRequest(cause.detail));
13551
13801
  return Effect.logWarning("hub request failed", { cause }).pipe(Effect.as(storeUnavailable));
13552
13802
  }));
@@ -13942,7 +14192,7 @@ const announce = Layer.effectDiscard(Effect.gen(function* () {
13942
14192
  });
13943
14193
  if (isRemoteReachableHost(config.host)) yield* Effect.logWarning("Hub is reachable beyond loopback. The bearer token is the only control — repeated auth failures are not rate limited.", { host: config.host });
13944
14194
  }));
13945
- const makeHubLayer = HttpRouter.serve(layer$58).pipe(Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
14195
+ const makeHubLayer = HttpRouter.serve(layer$58).pipe(Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(layer$59), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
13946
14196
  const runHub = Layer.launch(makeHubLayer);
13947
14197
  //#endregion
13948
14198
  //#region src/atomicWrite.ts
@@ -16126,7 +16376,7 @@ const make$58 = Effect.gen(function* () {
16126
16376
  const layer$53 = Layer.effect(AssetSync, make$58);
16127
16377
  //#endregion
16128
16378
  //#region src/cli/assets.ts
16129
- const SyncServicesLive = layer$53.pipe(Layer.provideMerge(layer$54), Layer.provideMerge(layer$55), Layer.provideMerge(layer$57.pipe(Layer.provide(layer$63))), Layer.provideMerge(layer$63), Layer.provideMerge(FetchHttpClient.layer));
16379
+ const SyncServicesLive = layer$53.pipe(Layer.provideMerge(layer$54), Layer.provideMerge(layer$55), Layer.provideMerge(layer$57.pipe(Layer.provide(layer$64))), Layer.provideMerge(layer$64), Layer.provideMerge(FetchHttpClient.layer));
16130
16380
  const runWithSync = (flags, run) => Effect.gen(function* () {
16131
16381
  const config = yield* resolveCliAuthConfig(flags, yield* GlobalFlag.LogLevel);
16132
16382
  return yield* Effect.gen(function* () {
@@ -16134,7 +16384,7 @@ const runWithSync = (flags, run) => Effect.gen(function* () {
16134
16384
  sync: yield* AssetSync,
16135
16385
  link: yield* HubLink
16136
16386
  });
16137
- }).pipe(Effect.provide(SyncServicesLive.pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")))));
16387
+ }).pipe(Effect.provide(SyncServicesLive.pipe(Layer.provide(layer$67(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")))));
16138
16388
  });
16139
16389
  const STATE_LABEL = {
16140
16390
  synced: "synced",
@@ -24582,7 +24832,7 @@ const RESTART_DELAY = Duration.seconds(2);
24582
24832
  /** Exact npm versions only — never dist-tags — so the acknowledgement names
24583
24833
  the version that was actually installed. Also keeps the value safe to
24584
24834
  pass to npm and embed in filesystem paths. */
24585
- const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
24835
+ const EXACT_VERSION_PATTERN$1 = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
24586
24836
  function normalizeEntryPath(entryPath) {
24587
24837
  return entryPath.replaceAll("\\", "/");
24588
24838
  }
@@ -24698,7 +24948,7 @@ const make$50 = Effect.fn("cloud.server_self_update.make")(function* (options) {
24698
24948
  if (capability === null) return yield* failWith("This server cannot update itself; relaunch it manually with the new version.");
24699
24949
  const activeMethod = capability;
24700
24950
  const targetVersion = input.targetVersion.trim();
24701
- if (!EXACT_VERSION_PATTERN.test(targetVersion)) return yield* failWith(`'${targetVersion}' is not an exact p4 version.`);
24951
+ if (!EXACT_VERSION_PATTERN$1.test(targetVersion)) return yield* failWith(`'${targetVersion}' is not an exact p4 version.`);
24702
24952
  if (yield* Ref.getAndSet(inFlight, true)) return yield* failWith("A server update is already in progress.");
24703
24953
  return yield* Effect.gen(function* () {
24704
24954
  const runtimePaths = yield* ensurePinnedRuntimeInstalled({
@@ -24727,6 +24977,7 @@ const make$50 = Effect.fn("cloud.server_self_update.make")(function* (options) {
24727
24977
  const homeDir = env.HOME ?? "";
24728
24978
  const unitPath = path.join(homeDir, ".config", "systemd", "user", BOOT_SERVICE_UNIT_FILE);
24729
24979
  const previousUnit = yield* fs.readFileString(unitPath).pipe(Effect.mapError((cause) => failWith("Could not read the current systemd unit.", cause)));
24980
+ const serviceOptions = yield* readServiceOptions({ baseDir: serverConfig.baseDir }).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
24730
24981
  const unit = renderBootServiceUnit({
24731
24982
  nodePath: host.execPath,
24732
24983
  p4EntryPath: runtimePaths.entryPath,
@@ -24734,7 +24985,7 @@ const make$50 = Effect.fn("cloud.server_self_update.make")(function* (options) {
24734
24985
  logPath: path.join(serverConfig.logsDir, "boot-service.log"),
24735
24986
  unitPath,
24736
24987
  workingDirectory: homeDir,
24737
- serviceEnvironment: []
24988
+ serviceEnvironment: serviceEnvironmentFor(serviceOptions)
24738
24989
  });
24739
24990
  yield* writeUnitAtomically(unitPath, unit).pipe(Effect.mapError((cause) => failWith("Could not update the systemd unit.", cause)));
24740
24991
  const reloadSystemd = Effect.fn("cloud.server_self_update.reload_systemd")(function* () {
@@ -26603,7 +26854,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (flags, run
26603
26854
  });
26604
26855
  yield* Console.log(output);
26605
26856
  }));
26606
- const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)));
26857
+ const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(Layer.provide(layer$67(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)));
26607
26858
  return yield* Effect.gen(function* () {
26608
26859
  const snapshot = yield* getOfflineSnapshot();
26609
26860
  const orchestrationEngine = yield* OrchestrationEngineService;
@@ -26614,7 +26865,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (flags, run
26614
26865
  });
26615
26866
  yield* Console.log(output);
26616
26867
  }).pipe(Effect.provide(offlineRuntimeLayer));
26617
- }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer, layer$42).pipe(Layer.provideMerge(FetchHttpClient.layer), Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
26868
+ }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer, layer$42).pipe(Layer.provideMerge(FetchHttpClient.layer), Layer.provide(layer$67(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
26618
26869
  });
26619
26870
  const projectAddCommand = Command.make("add", {
26620
26871
  ...projectLocationFlags,
@@ -30873,11 +31124,25 @@ function isResolvableContextWindowActivity(activity) {
30873
31124
  * newer updates still stream through and supersede the retained rows on the
30874
31125
  * client.
30875
31126
  */
31127
+ function withoutContextWindowBreakdown$1(activity) {
31128
+ const payload = asRecord$1(activity.payload);
31129
+ if (!payload || payload.breakdown === void 0) return activity;
31130
+ const { breakdown: _breakdown, ...rest } = payload;
31131
+ return {
31132
+ ...activity,
31133
+ payload: rest
31134
+ };
31135
+ }
30876
31136
  function dropStaleContextWindowActivities(activities) {
30877
31137
  const latestIndexByTurn = /* @__PURE__ */ new Map();
30878
31138
  for (let index = 0; index < activities.length; index += 1) if (isResolvableContextWindowActivity(activities[index])) latestIndexByTurn.set(activities[index].turnId, index);
30879
31139
  if (latestIndexByTurn.size === 0) return activities;
30880
- return activities.filter((activity, index) => !isResolvableContextWindowActivity(activity) || latestIndexByTurn.get(activity.turnId) === index);
31140
+ const newestIndex = Math.max(...latestIndexByTurn.values());
31141
+ return activities.flatMap((activity, index) => {
31142
+ if (!isResolvableContextWindowActivity(activity)) return [activity];
31143
+ if (latestIndexByTurn.get(activity.turnId) !== index) return [];
31144
+ return [index === newestIndex ? activity : withoutContextWindowBreakdown$1(activity)];
31145
+ });
30881
31146
  }
30882
31147
  function projectThreadDetailSnapshot(snapshot) {
30883
31148
  return {
@@ -31025,6 +31290,39 @@ const resolveTaskRepositoryKey = Effect.fn("taskBoard.resolveRepositoryKey")(fun
31025
31290
  return (yield* (yield* RepositoryIdentityResolver).resolve(project.value.workspaceRoot))?.canonicalKey ?? null;
31026
31291
  });
31027
31292
  /**
31293
+ * Everything that decides whether a task can be started, as one pure function.
31294
+ *
31295
+ * Kept out of the RPC handler because the three refusals are the whole of the
31296
+ * interesting behaviour and each is a different thing for the user to fix,
31297
+ * while the handler around them is two store calls and a dispatch. Phase 12's
31298
+ * orchestrator has to make the same three decisions before it may claim a task,
31299
+ * and it should reach the same verdicts by calling this rather than by reading
31300
+ * the handler and reimplementing it.
31301
+ *
31302
+ * The project is passed in as the repository returns it — including soft-deleted
31303
+ * rows, which is why `deletedAt` is checked here rather than assumed away.
31304
+ */
31305
+ const resolveTaskStart = (task, project) => {
31306
+ if (task.projectId === null) return {
31307
+ startable: false,
31308
+ reason: "no-project"
31309
+ };
31310
+ if (Option.isNone(project) || project.value.deletedAt !== null) return {
31311
+ startable: false,
31312
+ reason: "project-missing"
31313
+ };
31314
+ const model = project.value.defaultModelSelection;
31315
+ if (model === null) return {
31316
+ startable: false,
31317
+ reason: "no-model"
31318
+ };
31319
+ return {
31320
+ startable: true,
31321
+ projectId: task.projectId,
31322
+ model
31323
+ };
31324
+ };
31325
+ /**
31028
31326
  * Map a persistence failure to the caller-facing error.
31029
31327
  *
31030
31328
  * The underlying error carries SQL detail, which is noise to a client and, for
@@ -31036,6 +31334,54 @@ const toTaskStoreError = (operation) => (cause) => Effect.logWarning("task store
31036
31334
  cause
31037
31335
  }).pipe(Effect.andThen(Effect.fail(new TaskStoreError({ operation }))));
31038
31336
  //#endregion
31337
+ //#region ../../packages/shared/src/taskExecution.ts
31338
+ /**
31339
+ * The `auto` rendering: here is the task, do it.
31340
+ *
31341
+ * Two instructions carry their weight. The agent is told its thread is already
31342
+ * linked to the task, because otherwise its first useful move is to guess which
31343
+ * board row it is on. And it is told to leave `status` alone: the server moved
31344
+ * the task to `in_progress` when it started the thread, and where the work goes
31345
+ * after that is the user's call — the same rule the scoping prompt follows, and
31346
+ * the reason the board's columns still mean something.
31347
+ */
31348
+ const buildAutoPrompt = (task) => {
31349
+ const body = task.body.trim();
31350
+ return `You are picking up a task from the board. Do the work.
31351
+
31352
+ ## ${task.title}
31353
+
31354
+ ${body.length === 0 ? "_The task has no description. Read enough of the repository to work out what it means, and ask if it is genuinely ambiguous._" : body}
31355
+
31356
+ This thread is linked to task \`${task.taskId}\`, which is now In progress. \`task_current\` re-reads it if you need the fields again.
31357
+
31358
+ Record what you find with \`task_update\` as you go — a decision you made, a constraint you hit, something the task turned out to be wrong about. That is what makes the row worth reading afterwards. Leave \`status\` alone: moving work across the board is the user's call.
31359
+
31360
+ Start by reading the code the task concerns. If it turns out to be specified too poorly to act on, say so and stop rather than guessing at what was meant.`;
31361
+ };
31362
+ /**
31363
+ * Render a task into the prompt that seeds its thread.
31364
+ *
31365
+ * Exhaustive over the mode on purpose: adding a member to [TaskExecutionMode]
31366
+ * without a rendering should fail to compile rather than fall through to `auto`.
31367
+ */
31368
+ const buildTaskExecutionPrompt = (input) => {
31369
+ switch (input.mode) {
31370
+ case "auto": return buildAutoPrompt(input.task);
31371
+ }
31372
+ };
31373
+ const TASK_THREAD_TITLE_MAX_LENGTH = 60;
31374
+ /**
31375
+ * The thread's title, which is the task's own — a thread started from a task is
31376
+ * that task, and a derived-from-the-prompt title would read as a sentence of
31377
+ * instructions in the sidebar.
31378
+ */
31379
+ const buildTaskThreadTitle = (title) => {
31380
+ const compact = title.trim().replace(/\s+/gu, " ");
31381
+ if (compact.length === 0) return "Task";
31382
+ return compact.length > TASK_THREAD_TITLE_MAX_LENGTH ? `${compact.slice(0, TASK_THREAD_TITLE_MAX_LENGTH - 1).trimEnd()}…` : compact;
31383
+ };
31384
+ //#endregion
31039
31385
  //#region src/observability/RpcInstrumentation.ts
31040
31386
  const RPC_SPAN_PREFIX = "ws.rpc";
31041
31387
  const DEFAULT_RPC_SPAN_ATTRIBUTES = {
@@ -40762,6 +41108,7 @@ const RPC_REQUIRED_SCOPE = /* @__PURE__ */ new Map([
40762
41108
  [WS_METHODS.tasksCreate, AuthOrchestrationOperateScope],
40763
41109
  [WS_METHODS.tasksUpdate, AuthOrchestrationOperateScope],
40764
41110
  [WS_METHODS.tasksDelete, AuthOrchestrationOperateScope],
41111
+ [WS_METHODS.tasksStartThread, AuthOrchestrationOperateScope],
40765
41112
  [WS_METHODS.subscribeTasks, AuthOrchestrationReadScope],
40766
41113
  [WS_METHODS.hubGetSyncStatus, AuthOrchestrationReadScope],
40767
41114
  [WS_METHODS.hubConnect, AuthOrchestrationOperateScope],
@@ -40874,7 +41221,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
40874
41221
  const checkpointDiffQuery = yield* CheckpointDiffQuery;
40875
41222
  const keybindings = yield* Keybindings;
40876
41223
  const taskRepository = yield* TaskRepository;
40877
- yield* ProjectionProjectRepository;
41224
+ const projectionProjects = yield* ProjectionProjectRepository;
40878
41225
  const externalLauncher = yield* ExternalLauncher;
40879
41226
  const gitWorkflow = yield* GitWorkflowService;
40880
41227
  const review = yield* ReviewService;
@@ -41352,6 +41699,64 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
41352
41699
  return { task: patched.value };
41353
41700
  }), { "rpc.aggregate": "tasks" }),
41354
41701
  [WS_METHODS.tasksDelete]: ({ taskId }) => observeRpcEffect$1(WS_METHODS.tasksDelete, taskRepository.deleteById({ taskId }).pipe(Effect.catch(toTaskStoreError("tasks.delete")), Effect.as({})), { "rpc.aggregate": "tasks" }),
41702
+ [WS_METHODS.tasksStartThread]: ({ taskId }) => observeRpcEffect$1(WS_METHODS.tasksStartThread, Effect.gen(function* () {
41703
+ const found = yield* taskRepository.getById({ taskId }).pipe(Effect.catch(toTaskStoreError("tasks.startThread")));
41704
+ if (Option.isNone(found)) return yield* new TaskNotFoundError({ taskId });
41705
+ const task = found.value;
41706
+ const plan = resolveTaskStart(task, task.projectId === null ? Option.none() : yield* projectionProjects.getById({ projectId: task.projectId }).pipe(Effect.catch(toTaskStoreError("tasks.startThread.project"))));
41707
+ if (!plan.startable) return yield* new TaskNotStartableError({
41708
+ taskId,
41709
+ reason: plan.reason
41710
+ });
41711
+ const modelSelection = plan.model;
41712
+ const threadId = ThreadId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
41713
+ const createdAt = yield* nowIso$5;
41714
+ const threadTitle = buildTaskThreadTitle(task.title);
41715
+ const normalizedCommand = yield* normalizeDispatchCommand({
41716
+ type: "thread.turn.start",
41717
+ commandId: yield* serverCommandId("task-start-thread"),
41718
+ threadId,
41719
+ message: {
41720
+ messageId: MessageId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)),
41721
+ role: "user",
41722
+ text: buildTaskExecutionPrompt({
41723
+ mode: "auto",
41724
+ task: {
41725
+ taskId,
41726
+ title: task.title,
41727
+ body: task.body
41728
+ }
41729
+ }),
41730
+ attachments: []
41731
+ },
41732
+ modelSelection,
41733
+ titleSeed: threadTitle,
41734
+ runtimeMode: DEFAULT_RUNTIME_MODE$2,
41735
+ interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
41736
+ bootstrap: { createThread: {
41737
+ projectId: plan.projectId,
41738
+ title: threadTitle,
41739
+ modelSelection,
41740
+ runtimeMode: DEFAULT_RUNTIME_MODE$2,
41741
+ interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
41742
+ branch: null,
41743
+ worktreePath: null,
41744
+ createdAt
41745
+ } },
41746
+ createdAt
41747
+ });
41748
+ yield* dispatchNormalizedCommand(normalizedCommand);
41749
+ const patched = yield* taskRepository.patch({
41750
+ taskId,
41751
+ threadId,
41752
+ status: "in_progress"
41753
+ }, yield* nowIso$5).pipe(Effect.catch(toTaskStoreError("tasks.startThread.link")));
41754
+ if (Option.isNone(patched)) return yield* new TaskNotFoundError({ taskId });
41755
+ return {
41756
+ task: patched.value,
41757
+ threadId
41758
+ };
41759
+ }), { "rpc.aggregate": "tasks" }),
41355
41760
  [WS_METHODS.subscribeTasks]: (_input) => observeRpcStreamEffect$1(WS_METHODS.subscribeTasks, Effect.succeed(taskRepository.streamWithSnapshot.pipe(Stream.catch((cause) => Stream.fromEffect(toTaskStoreError("tasks.subscribe")(cause))))), { "rpc.aggregate": "tasks" }),
41356
41761
  [WS_METHODS.hubGetSyncStatus]: (_input) => observeRpcEffect$1(WS_METHODS.hubGetSyncStatus, assetSync.status, { "rpc.aggregate": "hub" }),
41357
41762
  [WS_METHODS.hubConnect]: (input) => observeRpcEffect$1(WS_METHODS.hubConnect, hubLink.connect(input).pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause.message })), Effect.andThen(assetSync.status)), { "rpc.aggregate": "hub" }),
@@ -43610,7 +44015,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
43610
44015
  };
43611
44016
  });
43612
44017
  var OpenCodeRuntime = class extends Context.Service()("@p4code/cli/provider/opencodeRuntime") {};
43613
- const OpenCodeRuntimeLive = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(Layer.provide(layer$67));
44018
+ const OpenCodeRuntimeLive = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(Layer.provide(layer$68));
43614
44019
  //#endregion
43615
44020
  //#region src/textGeneration/TextGenerationUtils.ts
43616
44021
  const isTextGenerationError$2 = Schema$1.is(TextGenerationError);
@@ -45149,13 +45554,178 @@ function normalizeClaudeActiveTokenUsage(value, contextWindow, totalProcessedTok
45149
45554
  ...totalProcessedTokens !== void 0 ? { totalProcessedTokens } : {}
45150
45555
  });
45151
45556
  }
45557
+ /**
45558
+ * The contract's string fields are non-empty, and this payload is decoded on the
45559
+ * way out, so an entry the SDK reports with a blank name would fail the whole
45560
+ * event rather than lose one row. Drop those instead.
45561
+ */
45562
+ function trimmedName(value) {
45563
+ if (typeof value !== "string") return;
45564
+ const trimmed = value.trim();
45565
+ return trimmed.length > 0 ? trimmed : void 0;
45566
+ }
45567
+ function mapNamedCosts(entries) {
45568
+ if (!entries || entries.length === 0) return;
45569
+ const mapped = entries.flatMap((entry) => {
45570
+ const name = trimmedName(entry.name);
45571
+ const tokens = finiteNonNegativeInteger(entry.tokens);
45572
+ return name !== void 0 && tokens !== void 0 ? [{
45573
+ name,
45574
+ tokens
45575
+ }] : [];
45576
+ });
45577
+ return mapped.length > 0 ? mapped : void 0;
45578
+ }
45579
+ /**
45580
+ * Keeps the per-source attribution the SDK already computes on every turn. The
45581
+ * `gridRows` and `color` fields are dropped: they describe how the vendor CLI
45582
+ * paints its own meter, and this UI has its own.
45583
+ */
45584
+ function claudeContextWindowBreakdown(value) {
45585
+ const model = trimmedName(value.model);
45586
+ const categories = value.categories.flatMap((category) => {
45587
+ const name = trimmedName(category.name);
45588
+ const tokens = finiteNonNegativeInteger(category.tokens);
45589
+ if (name === void 0 || tokens === void 0) return [];
45590
+ return [{
45591
+ name,
45592
+ tokens,
45593
+ ...category.isDeferred !== void 0 ? { isDeferred: category.isDeferred } : {}
45594
+ }];
45595
+ });
45596
+ const mcpTools = value.mcpTools.flatMap((tool) => {
45597
+ const name = trimmedName(tool.name);
45598
+ const serverName = trimmedName(tool.serverName);
45599
+ const tokens = finiteNonNegativeInteger(tool.tokens);
45600
+ if (name === void 0 || serverName === void 0 || tokens === void 0) return [];
45601
+ return [{
45602
+ name,
45603
+ serverName,
45604
+ tokens,
45605
+ ...tool.isLoaded !== void 0 ? { isLoaded: tool.isLoaded } : {}
45606
+ }];
45607
+ });
45608
+ const memoryFiles = value.memoryFiles.flatMap((file) => {
45609
+ const path = trimmedName(file.path);
45610
+ const tokens = finiteNonNegativeInteger(file.tokens);
45611
+ if (path === void 0 || tokens === void 0) return [];
45612
+ const type = trimmedName(file.type);
45613
+ return [{
45614
+ path,
45615
+ tokens,
45616
+ ...type !== void 0 ? { type } : {}
45617
+ }];
45618
+ });
45619
+ const agents = value.agents.flatMap((agent) => {
45620
+ const agentType = trimmedName(agent.agentType);
45621
+ const tokens = finiteNonNegativeInteger(agent.tokens);
45622
+ if (agentType === void 0 || tokens === void 0) return [];
45623
+ const source = trimmedName(agent.source);
45624
+ return [{
45625
+ agentType,
45626
+ tokens,
45627
+ ...source !== void 0 ? { source } : {}
45628
+ }];
45629
+ });
45630
+ const deferredBuiltinTools = value.deferredBuiltinTools?.flatMap((tool) => {
45631
+ const name = trimmedName(tool.name);
45632
+ const tokens = finiteNonNegativeInteger(tool.tokens);
45633
+ return name !== void 0 && tokens !== void 0 ? [{
45634
+ name,
45635
+ tokens,
45636
+ isLoaded: tool.isLoaded
45637
+ }] : [];
45638
+ });
45639
+ const skillsSource = value.skills;
45640
+ const skillEntries = skillsSource?.skillFrontmatter.flatMap((skill) => {
45641
+ const name = trimmedName(skill.name);
45642
+ const tokens = finiteNonNegativeInteger(skill.tokens);
45643
+ if (name === void 0 || tokens === void 0) return [];
45644
+ const source = trimmedName(skill.source);
45645
+ return [{
45646
+ name,
45647
+ tokens,
45648
+ ...source !== void 0 ? { source } : {}
45649
+ }];
45650
+ });
45651
+ const skills = skillsSource === void 0 ? void 0 : {
45652
+ totalSkills: finiteNonNegativeInteger(skillsSource.totalSkills) ?? 0,
45653
+ includedSkills: finiteNonNegativeInteger(skillsSource.includedSkills) ?? 0,
45654
+ tokens: finiteNonNegativeInteger(skillsSource.tokens) ?? 0,
45655
+ ...skillEntries && skillEntries.length > 0 ? { entries: skillEntries } : {}
45656
+ };
45657
+ const slashCommandsSource = value.slashCommands;
45658
+ const slashCommands = slashCommandsSource === void 0 ? void 0 : {
45659
+ totalCommands: finiteNonNegativeInteger(slashCommandsSource.totalCommands) ?? 0,
45660
+ includedCommands: finiteNonNegativeInteger(slashCommandsSource.includedCommands) ?? 0,
45661
+ tokens: finiteNonNegativeInteger(slashCommandsSource.tokens) ?? 0
45662
+ };
45663
+ const messagesSource = value.messageBreakdown;
45664
+ const toolCallsByType = messagesSource?.toolCallsByType.flatMap((entry) => {
45665
+ const name = trimmedName(entry.name);
45666
+ const callTokens = finiteNonNegativeInteger(entry.callTokens);
45667
+ const resultTokens = finiteNonNegativeInteger(entry.resultTokens);
45668
+ return name !== void 0 && callTokens !== void 0 && resultTokens !== void 0 ? [{
45669
+ name,
45670
+ callTokens,
45671
+ resultTokens
45672
+ }] : [];
45673
+ });
45674
+ const attachmentsByType = mapNamedCosts(messagesSource?.attachmentsByType);
45675
+ const messages = messagesSource === void 0 ? void 0 : {
45676
+ toolCallTokens: finiteNonNegativeInteger(messagesSource.toolCallTokens) ?? 0,
45677
+ toolResultTokens: finiteNonNegativeInteger(messagesSource.toolResultTokens) ?? 0,
45678
+ attachmentTokens: finiteNonNegativeInteger(messagesSource.attachmentTokens) ?? 0,
45679
+ assistantMessageTokens: finiteNonNegativeInteger(messagesSource.assistantMessageTokens) ?? 0,
45680
+ userMessageTokens: finiteNonNegativeInteger(messagesSource.userMessageTokens) ?? 0,
45681
+ redirectedContextTokens: finiteNonNegativeInteger(messagesSource.redirectedContextTokens) ?? 0,
45682
+ unattributedTokens: finiteNonNegativeInteger(messagesSource.unattributedTokens) ?? 0,
45683
+ ...toolCallsByType && toolCallsByType.length > 0 ? { toolCallsByType } : {},
45684
+ ...attachmentsByType !== void 0 ? { attachmentsByType } : {}
45685
+ };
45686
+ const systemTools = mapNamedCosts(value.systemTools);
45687
+ const systemPromptSections = mapNamedCosts(value.systemPromptSections);
45688
+ const autoCompactThreshold = finiteNonNegativeInteger(value.autoCompactThreshold);
45689
+ const breakdown = {
45690
+ ...model !== void 0 ? { model } : {},
45691
+ ...categories.length > 0 ? { categories } : {},
45692
+ ...mcpTools.length > 0 ? { mcpTools } : {},
45693
+ ...skills !== void 0 ? { skills } : {},
45694
+ ...memoryFiles.length > 0 ? { memoryFiles } : {},
45695
+ ...agents.length > 0 ? { agents } : {},
45696
+ ...systemTools !== void 0 ? { systemTools } : {},
45697
+ ...deferredBuiltinTools && deferredBuiltinTools.length > 0 ? { deferredBuiltinTools } : {},
45698
+ ...systemPromptSections !== void 0 ? { systemPromptSections } : {},
45699
+ ...slashCommands !== void 0 ? { slashCommands } : {},
45700
+ ...messages !== void 0 ? { messages } : {},
45701
+ ...autoCompactThreshold !== void 0 ? { autoCompactThreshold } : {}
45702
+ };
45703
+ return Object.keys(breakdown).length > 0 ? breakdown : void 0;
45704
+ }
45705
+ /**
45706
+ * Drops the attribution from a snapshot that is being carried forward. The token
45707
+ * counts still describe the thread, but the breakdown was measured against a
45708
+ * turn that has since moved on. A stale breakdown presented as the current one
45709
+ * is worse than none, because nothing about it looks stale.
45710
+ */
45711
+ function withoutContextWindowBreakdown(usage) {
45712
+ if (!usage || usage.breakdown === void 0) return usage;
45713
+ const { breakdown: _breakdown, ...rest } = usage;
45714
+ return rest;
45715
+ }
45152
45716
  function normalizeClaudeContextUsageApiSnapshot(value, totalProcessedTokens) {
45153
- return makeClaudeTokenUsageSnapshot({
45717
+ const snapshot = makeClaudeTokenUsageSnapshot({
45154
45718
  activeTokens: value.totalTokens,
45155
45719
  contextWindow: value.maxTokens,
45156
45720
  ...totalProcessedTokens !== void 0 ? { totalProcessedTokens } : {},
45157
45721
  compactsAutomatically: value.isAutoCompactEnabled
45158
45722
  });
45723
+ if (!snapshot) return;
45724
+ const breakdown = claudeContextWindowBreakdown(value);
45725
+ return breakdown !== void 0 ? {
45726
+ ...snapshot,
45727
+ breakdown
45728
+ } : snapshot;
45159
45729
  }
45160
45730
  function compactBoundaryTokenUsageSnapshot(message, contextWindow, totalProcessedTokens) {
45161
45731
  const metadata = message.compact_metadata;
@@ -45948,7 +46518,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
45948
46518
  const resultHasActiveUsage = resultUsageRecord !== void 0 && (hasResultUsageIteration || claudeUsageInputTokens(resultUsageRecord) + claudeUsageOutputTokens(resultUsageRecord) > 0);
45949
46519
  const resultTotalOnly = resultUsageRecord !== void 0 && !resultHasActiveUsage && claudeTotalProcessedTokens(resultUsageRecord) !== void 0;
45950
46520
  const resultIterationSnapshot = resultUsageRecord ? normalizeClaudeActiveTokenUsage(resultUsageRecord, maxTokens, accumulatedTotalProcessedTokens ?? context.lastKnownTotalProcessedTokens) : void 0;
45951
- const lastGoodUsage = context.lastKnownTokenUsage;
46521
+ const lastGoodUsage = withoutContextWindowBreakdown(context.lastKnownTokenUsage);
45952
46522
  const usageSnapshot = contextUsageSnapshot ?? (resultTotalOnly && lastGoodUsage ? {
45953
46523
  ...lastGoodUsage,
45954
46524
  ...typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0 ? { maxTokens } : {},
@@ -82703,9 +83273,9 @@ const WorkspaceEntriesLayerLive = layer$26.pipe(Layer.provide(layer$42));
82703
83273
  const WorkspaceFileSystemLayerLive = layer$25.pipe(Layer.provide(layer$42), Layer.provide(WorkspaceEntriesLayerLive));
82704
83274
  const WorkspaceLayerLive = Layer.mergeAll(layer$42, WorkspaceEntriesLayerLive, WorkspaceFileSystemLayerLive);
82705
83275
  const ProjectFaviconResolverLayerLive = layer$40.pipe(Layer.provide(layer$42), Layer.provide(layer$41));
82706
- const AuthLayerLive = layer$59.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$63));
83276
+ const AuthLayerLive = layer$60.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$64));
82707
83277
  const ProviderRuntimeLayerLive = ProviderSessionReaperLive.pipe(Layer.provideMerge(ProviderLayerLive), Layer.provideMerge(OrchestrationLayerLive));
82708
- const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(CheckpointingLayerLive), Layer.provideMerge(SourceControlProviderRegistryLayerLive), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)), Layer.provideMerge(PersistenceLayerLive), Layer.provideMerge(layer$50), Layer.provideMerge(ProviderRegistryLive), Layer.provideMerge(ProviderInstanceRegistryHydrationLive), Layer.provideMerge(ProviderEventLoggersLive), Layer.provideMerge(OpenCodeRuntimeLive), Layer.provideMerge(layer$57.pipe(Layer.provide(layer$63))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(layer$51), Layer.provideMerge(layer$44), Layer.provideMerge(AuthLayerLive), Layer.provideMerge(layer$56), Layer.provideMerge(layer$63)).pipe(Layer.provideMerge(layer$8), Layer.provideMerge(layer$7), Layer.provideMerge(layer$6), Layer.provideMerge(layer$47), Layer.provideMerge(layer$49), Layer.provideMerge(layer$48), Layer.provide(layer$67));
83278
+ const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(CheckpointingLayerLive), Layer.provideMerge(SourceControlProviderRegistryLayerLive), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)), Layer.provideMerge(PersistenceLayerLive), Layer.provideMerge(layer$50), Layer.provideMerge(ProviderRegistryLive), Layer.provideMerge(ProviderInstanceRegistryHydrationLive), Layer.provideMerge(ProviderEventLoggersLive), Layer.provideMerge(OpenCodeRuntimeLive), Layer.provideMerge(layer$57.pipe(Layer.provide(layer$64))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(layer$51), Layer.provideMerge(layer$44), Layer.provideMerge(AuthLayerLive), Layer.provideMerge(layer$56), Layer.provideMerge(layer$64)).pipe(Layer.provideMerge(layer$8), Layer.provideMerge(layer$7), Layer.provideMerge(layer$6), Layer.provideMerge(layer$47), Layer.provideMerge(layer$49), Layer.provideMerge(layer$48), Layer.provide(layer$68));
82709
83279
  /**
82710
83280
  * Hub asset sync.
82711
83281
  *
@@ -82773,6 +83343,151 @@ const serveCommand = Command.make("serve", { ...sharedServerCommandFlags }).pipe
82773
83343
  forceAutoBootstrapProjectFromCwd: false
82774
83344
  })));
82775
83345
  //#endregion
83346
+ //#region src/service/cliSelfUpdate.ts
83347
+ /**
83348
+ * Upgrading the globally installed CLI, then the service that runs it.
83349
+ *
83350
+ * Two steps that have to happen in that order and from different binaries, and
83351
+ * getting the order wrong is silent: `service update` renders the unit from the
83352
+ * *running* CLI, and pins that version into the runtime directory. Run the
83353
+ * reconcile from the old process and you install a new CLI and then carefully
83354
+ * point the service back at the old one.
83355
+ *
83356
+ * So the second step re-executes the newly installed binary. That is also why
83357
+ * this is not `ServerSelfUpdate`: that path respawns the server in place, which
83358
+ * it explicitly refuses to do under launchd — KeepAlive would restart the
83359
+ * process being replaced and leave two servers on one port. Under a supervisor
83360
+ * the honest sequence is install, then let the supervisor restart, which is
83361
+ * exactly what `service update` does.
83362
+ *
83363
+ * @module service/cliSelfUpdate
83364
+ */
83365
+ /** npm can take minutes when a native dependency compiles from source. */
83366
+ const CLI_INSTALL_TIMEOUT_MS = 600 * 1e3;
83367
+ const PACKAGE_NAME = "@p4code/cli";
83368
+ /**
83369
+ * Exact versions or the two dist-tags, and nothing else.
83370
+ *
83371
+ * The value is handed to npm and printed back as "installed X". An arbitrary
83372
+ * string could be a git URL or a local path, which npm would happily install
83373
+ * and which would make the acknowledgement a lie.
83374
+ */
83375
+ const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?$/u;
83376
+ const isInstallableVersion = (value) => value === "latest" || value === "next" || EXACT_VERSION_PATTERN.test(value);
83377
+ var CliSelfUpdateError = class extends Schema$1.TaggedErrorClass()("CliSelfUpdateError", {
83378
+ step: Schema$1.String,
83379
+ detail: Schema$1.optional(Schema$1.String)
83380
+ }) {
83381
+ get message() {
83382
+ return this.detail === void 0 ? `Self-update failed while ${this.step}.` : `Self-update failed while ${this.step}: ${this.detail}`;
83383
+ }
83384
+ };
83385
+ /**
83386
+ * Where `npm install --global` put the executable.
83387
+ *
83388
+ * Asked of npm rather than assumed, because the answer differs between a
83389
+ * Homebrew node, an nvm one and a system one — and the whole point of this step
83390
+ * is to run the binary that was just written, not the one on this process's
83391
+ * PATH, which a shell may have cached.
83392
+ */
83393
+ const resolveGlobalBinary = Effect.fn("cli.selfUpdate.resolveGlobalBinary")(function* (binaryName) {
83394
+ const result = yield* (yield* ProcessRunner).run({
83395
+ command: "npm",
83396
+ args: ["prefix", "--global"]
83397
+ }).pipe(Effect.mapError((cause) => new CliSelfUpdateError({
83398
+ step: "locating the npm global prefix",
83399
+ detail: String(cause)
83400
+ })));
83401
+ if (result.code !== 0) return yield* new CliSelfUpdateError({
83402
+ step: "locating the npm global prefix",
83403
+ detail: `npm exited with code ${String(result.code)}`
83404
+ });
83405
+ const prefix = result.stdout.trim();
83406
+ if (prefix.length === 0) return yield* new CliSelfUpdateError({
83407
+ step: "locating the npm global prefix",
83408
+ detail: "npm printed no prefix"
83409
+ });
83410
+ return `${prefix}/bin/${binaryName}`;
83411
+ });
83412
+ const installGlobalCli = Effect.fn("cli.selfUpdate.installGlobalCli")(function* (input) {
83413
+ const runner = yield* ProcessRunner;
83414
+ const step = `installing ${PACKAGE_NAME}@${input.version}`;
83415
+ const result = yield* runner.run({
83416
+ command: "npm",
83417
+ args: [
83418
+ "install",
83419
+ "--global",
83420
+ "--no-fund",
83421
+ "--no-audit",
83422
+ `${PACKAGE_NAME}@${input.version}`
83423
+ ],
83424
+ timeout: CLI_INSTALL_TIMEOUT_MS
83425
+ }).pipe(Effect.mapError((cause) => new CliSelfUpdateError({
83426
+ step,
83427
+ detail: String(cause)
83428
+ })));
83429
+ if (result.code !== 0) return yield* new CliSelfUpdateError({
83430
+ step,
83431
+ detail: result.stderr.trim().length > 0 ? result.stderr.trim() : `exit code ${String(result.code)}`
83432
+ });
83433
+ });
83434
+ /**
83435
+ * Ask the freshly installed binary what version it is.
83436
+ *
83437
+ * A completed npm install can still be unusable — a native dependency that
83438
+ * failed to build, a Node too old — and it is better to find that out here,
83439
+ * while the service is still running the version that works, than after
83440
+ * pointing the unit at it.
83441
+ */
83442
+ const readInstalledVersion = Effect.fn("cli.selfUpdate.readInstalledVersion")(function* (binaryPath) {
83443
+ const runner = yield* ProcessRunner;
83444
+ const step = "verifying the installed CLI";
83445
+ const result = yield* runner.run({
83446
+ command: binaryPath,
83447
+ args: ["--version"]
83448
+ }).pipe(Effect.mapError((cause) => new CliSelfUpdateError({
83449
+ step,
83450
+ detail: String(cause)
83451
+ })));
83452
+ if (result.code !== 0) return yield* new CliSelfUpdateError({
83453
+ step,
83454
+ detail: `the installed CLI exited with code ${String(result.code)}`
83455
+ });
83456
+ const match = /(\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?)/u.exec(result.stdout);
83457
+ if (match?.[1] === void 0) return yield* new CliSelfUpdateError({
83458
+ step,
83459
+ detail: "the installed CLI did not report a version"
83460
+ });
83461
+ return match[1];
83462
+ });
83463
+ /**
83464
+ * Hand the rest of the work to the new binary.
83465
+ *
83466
+ * `service update` from *this* process would pin this process's version. The
83467
+ * new binary reconciles to itself, which is the only way the unit ends up
83468
+ * pointing at what was just installed.
83469
+ */
83470
+ const reconcileWithNewBinary = Effect.fn("cli.selfUpdate.reconcileWithNewBinary")(function* (input) {
83471
+ const runner = yield* ProcessRunner;
83472
+ const step = "updating the service to the new version";
83473
+ const result = yield* runner.run({
83474
+ command: input.binaryPath,
83475
+ args: [
83476
+ "service",
83477
+ "update",
83478
+ ...input.baseDir === void 0 ? [] : ["--base-dir", input.baseDir]
83479
+ ],
83480
+ timeout: CLI_INSTALL_TIMEOUT_MS
83481
+ }).pipe(Effect.mapError((cause) => new CliSelfUpdateError({
83482
+ step,
83483
+ detail: String(cause)
83484
+ })));
83485
+ if (result.code !== 0) return yield* new CliSelfUpdateError({
83486
+ step,
83487
+ detail: result.stderr.trim().length > 0 ? result.stderr.trim() : `exit code ${String(result.code)}`
83488
+ });
83489
+ });
83490
+ //#endregion
82776
83491
  //#region src/cli/service.ts
82777
83492
  const isBootServiceCommandError = Schema$1.is(BootServiceCommandError);
82778
83493
  const bootServiceLayer = (config) => layer$46({
@@ -82918,6 +83633,39 @@ const serviceRestartCommand = Command.make("restart", projectLocationFlags).pipe
82918
83633
  yield* service.restart;
82919
83634
  yield* Console.log("Restarted the P4Code service.");
82920
83635
  }))));
83636
+ /**
83637
+ * Install a newer CLI, then point the service at it.
83638
+ *
83639
+ * Deliberately not `ServerSelfUpdate`, which the server uses to replace itself
83640
+ * in place: that path refuses to run under launchd, because KeepAlive would
83641
+ * restart the process being replaced and leave two servers on one port. Under a
83642
+ * supervisor the honest sequence is install, then reconcile and let the
83643
+ * supervisor restart — which is what this does, from the new binary.
83644
+ */
83645
+ const serviceSelfUpdateCommand = Command.make("self-update", {
83646
+ ...projectLocationFlags,
83647
+ version: Flag.string("version").pipe(Flag.withDescription("Version or dist-tag to install (default latest)."), Flag.optional)
83648
+ }).pipe(Command.withDescription("Install the newest @p4code/cli and update the service to run it."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
83649
+ const requested = Option.getOrElse(flags.version, () => "latest");
83650
+ if (!isInstallableVersion(requested)) {
83651
+ yield* Console.error(`'${requested}' is not a version or dist-tag. Use an exact version, or latest.`);
83652
+ return;
83653
+ }
83654
+ yield* Console.log(`Installing ${PACKAGE_NAME}@${requested}…`);
83655
+ yield* installGlobalCli({ version: requested });
83656
+ const binaryPath = yield* resolveGlobalBinary("p4c");
83657
+ const installed = yield* readInstalledVersion(binaryPath);
83658
+ if (installed === version) {
83659
+ yield* Console.log(`Already running @p4code/cli@${installed}; nothing to update.`);
83660
+ return;
83661
+ }
83662
+ yield* Console.log(`Installed @p4code/cli@${installed}; updating the service…`);
83663
+ yield* reconcileWithNewBinary({
83664
+ binaryPath,
83665
+ baseDir: void 0
83666
+ });
83667
+ yield* Console.log(`Service now runs @p4code/cli@${installed}.`);
83668
+ }))));
82921
83669
  const serviceStatusCommand = Command.make("status", projectLocationFlags).pipe(Command.withDescription("Show whether the P4Code background service is installed."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
82922
83670
  const service = yield* BootService;
82923
83671
  yield* Console.log(formatServiceStatus(yield* service.status, version));
@@ -82942,11 +83690,12 @@ const serviceCommand = Command.make("service").pipe(Command.withDescription("Man
82942
83690
  serviceUninstallCommand,
82943
83691
  serviceUpdateCommand,
82944
83692
  serviceRestartCommand,
83693
+ serviceSelfUpdateCommand,
82945
83694
  serviceStatusCommand
82946
83695
  ]));
82947
83696
  //#endregion
82948
83697
  //#region src/bin.ts
82949
- const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, layer$67, FetchHttpClient.layer);
83698
+ const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, layer$68, FetchHttpClient.layer);
82950
83699
  /**
82951
83700
  * Prints an update reminder before dispatching, at most once a day.
82952
83701
  *