@p4code/cli 0.5.12 → 0.5.13

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.
package/dist/bin.mjs CHANGED
@@ -265,7 +265,7 @@ const make$120 = () => {
265
265
  const layer$122 = Layer.sync(NetService, make$120);
266
266
  //#endregion
267
267
  //#region package.json
268
- var version$1 = "0.5.12";
268
+ var version$1 = "0.5.13";
269
269
  //#endregion
270
270
  //#region src/config.ts
271
271
  /**
@@ -12516,6 +12516,16 @@ const ThreadRenameResult = Schema$1.Struct({
12516
12516
  title: TrimmedNonEmptyString
12517
12517
  });
12518
12518
  const ThreadPlanUpdateInput = Schema$1.Struct({
12519
+ /**
12520
+ * The thread whose banner this plan belongs to.
12521
+ *
12522
+ * Omitted means the caller's own thread, which is every ordinary use. A
12523
+ * Fusion supervisor names its paired builder here: the plan is the
12524
+ * supervisor's to author, but it describes the builder's work, so it has to
12525
+ * land on the builder's banner. Only a thread this session holds a watch
12526
+ * grant for is accepted.
12527
+ */
12528
+ threadId: Schema$1.optional(ThreadId.annotate({ description: "The thread to update. Omit for this session's own thread. Only a thread this session may watch can be named here." })),
12519
12529
  explanation: Schema$1.optional(TrimmedNonEmptyString),
12520
12530
  plan: Schema$1.Array(Schema$1.Struct({
12521
12531
  step: TrimmedNonEmptyString,
@@ -12812,7 +12822,22 @@ const ThreadReviewItem = Schema$1.Union([Schema$1.Struct({
12812
12822
  /** Apply append/replace by message identity across page boundaries. */
12813
12823
  operation: Schema$1.Literals(["append", "replace"]),
12814
12824
  complete: Schema$1.Boolean,
12815
- text: Schema$1.String
12825
+ text: Schema$1.String,
12826
+ /**
12827
+ * What the message carried besides text.
12828
+ *
12829
+ * Present only when there was something: a message whose fragments are
12830
+ * merged by identity would otherwise lose every trace of an image or a
12831
+ * document the builder attached. Bounded by
12832
+ * {@link THREAD_REVIEW_ITEM_MAX_ATTACHMENTS} so the item stays trimmable -
12833
+ * only its text shrinks under the byte cap, so nothing else may be able to
12834
+ * grow without limit.
12835
+ */
12836
+ attachments: Schema$1.optionalKey(Schema$1.Array(Schema$1.Struct({
12837
+ type: Schema$1.String,
12838
+ name: Schema$1.String,
12839
+ sizeBytes: NonNegativeInt
12840
+ })))
12816
12841
  }), Schema$1.Struct({
12817
12842
  ...ReviewSourceFields,
12818
12843
  kind: Schema$1.Literal("event"),
@@ -12857,6 +12882,84 @@ const ThreadWatchReviewResult = Schema$1.Struct({
12857
12882
  */
12858
12883
  summary: Schema$1.optionalKey(Schema$1.NullOr(ThreadReviewSummary))
12859
12884
  });
12885
+ /**
12886
+ * How a review ended, from the supervisor's own account of it.
12887
+ *
12888
+ * Four states rather than a boolean because they are acted on differently: a
12889
+ * summary that arrived is evidence, a fallback is a working review that cost
12890
+ * full context, a failure is a misconfiguration worth fixing, and a read with
12891
+ * no report at all is the only one that cannot be told from a supervisor that
12892
+ * simply never reported - which is why the absence has to be visible too.
12893
+ */
12894
+ const ThreadEvidenceReportOutcome = Schema$1.Literals([
12895
+ "summarized",
12896
+ "fallback",
12897
+ "failed"
12898
+ ]);
12899
+ /** Bounded so one report cannot dominate a thread's activity storage. */
12900
+ const THREAD_EVIDENCE_REPORT_MAX_CHARS = 8e3;
12901
+ const ThreadEvidenceReportInput = Schema$1.Struct({
12902
+ /** The watched thread the summary covers, not the reporting thread. */
12903
+ threadId: ThreadId,
12904
+ firstSequence: NonNegativeInt,
12905
+ lastSequence: NonNegativeInt,
12906
+ /**
12907
+ * The fixed review boundary the pages were read against.
12908
+ *
12909
+ * Identity, not description: the evidence-read rows are keyed on it, so a
12910
+ * report that guessed from its own last sequence would look for a baseline
12911
+ * that never existed whenever the range ended on a filtered event.
12912
+ */
12913
+ throughSequence: NonNegativeInt,
12914
+ outcome: ThreadEvidenceReportOutcome,
12915
+ /** The summarizer's exact text. Required when the outcome is summarized. */
12916
+ summary: Schema$1.optional(Schema$1.String),
12917
+ /** The model the subagent ran on, when one was spawned. */
12918
+ model: Schema$1.optional(Schema$1.String),
12919
+ /** Why the summary is missing or degraded. Required unless summarized. */
12920
+ reason: Schema$1.optional(Schema$1.String)
12921
+ });
12922
+ /**
12923
+ * The supervisor's own context around the read, correlated by the server.
12924
+ *
12925
+ * Self-reported numbers would be worth little, so these are read from the
12926
+ * reporting thread's projected context-window rows: `before` is what was
12927
+ * recorded when the evidence read happened, `after` what is recorded when the
12928
+ * report lands. `delta` can be negative when a compaction ran in between, and
12929
+ * is null whenever no read-time baseline exists.
12930
+ */
12931
+ const ThreadEvidenceReportContext = Schema$1.Struct({
12932
+ /**
12933
+ * Null when no reading was recorded at read time.
12934
+ *
12935
+ * Never the report-time value standing in for itself: a delta of zero read
12936
+ * as measurement would be a lie about a review that cost context.
12937
+ */
12938
+ usedTokensBefore: Schema$1.NullOr(NonNegativeInt),
12939
+ usedTokensAfter: NonNegativeInt,
12940
+ /** Null exactly when `usedTokensBefore` is. */
12941
+ deltaTokens: Schema$1.NullOr(Schema$1.Number),
12942
+ maxTokens: Schema$1.NullOr(NonNegativeInt),
12943
+ capacityPercent: Schema$1.NullOr(Schema$1.Number)
12944
+ });
12945
+ Schema$1.Struct({
12946
+ watchedThreadId: ThreadId,
12947
+ firstSequence: NonNegativeInt,
12948
+ lastSequence: NonNegativeInt,
12949
+ throughSequence: NonNegativeInt,
12950
+ outcome: ThreadEvidenceReportOutcome,
12951
+ summary: Schema$1.NullOr(Schema$1.String),
12952
+ /** True when a snapshot dropped the text of a superseded report. */
12953
+ summaryDropped: Schema$1.optionalKey(Schema$1.Boolean),
12954
+ model: Schema$1.NullOr(Schema$1.String),
12955
+ reason: Schema$1.NullOr(Schema$1.String),
12956
+ /** Null when no context-window row exists yet for the reporting thread. */
12957
+ context: Schema$1.NullOr(ThreadEvidenceReportContext)
12958
+ });
12959
+ const ThreadEvidenceReportResult = Schema$1.Struct({
12960
+ recorded: Schema$1.Boolean,
12961
+ context: Schema$1.NullOr(ThreadEvidenceReportContext)
12962
+ });
12860
12963
  var WatchToolUnavailableError = class extends Schema$1.TaggedErrorClass()("WatchToolUnavailableError", {
12861
12964
  capability: Schema$1.Literal("watch"),
12862
12965
  environmentId: EnvironmentId,
@@ -20405,12 +20508,46 @@ function dropSupersededToolUpdatedActivities(activities) {
20405
20508
  return completionIndex === void 0 || completionIndex <= index || !completionPreservesUpdate(activity, activities[completionIndex]);
20406
20509
  });
20407
20510
  }
20511
+ /**
20512
+ * Keeps only the newest evidence report's text per watched thread.
20513
+ *
20514
+ * A report carries the summarizer's exact words, and a snapshot retains
20515
+ * hundreds of activities: left whole, a long-running pair would put megabytes
20516
+ * of superseded diagnostics on the wire on every reconnect. Superseded rows
20517
+ * keep their outcome, range and context - everything the work log renders
20518
+ * collapsed - and lose only the body, flagged so the client can say the text
20519
+ * is no longer retained rather than that the review produced none.
20520
+ */
20521
+ function dropSupersededEvidenceSummaries(activities) {
20522
+ const latestIndexByWatched = /* @__PURE__ */ new Map();
20523
+ for (let index = 0; index < activities.length; index += 1) {
20524
+ const activity = activities[index];
20525
+ if (activity.kind !== "fusion.evidence.summary") continue;
20526
+ const watchedThreadId = asRecord$7(activity.payload)?.watchedThreadId;
20527
+ if (typeof watchedThreadId === "string") latestIndexByWatched.set(watchedThreadId, index);
20528
+ }
20529
+ if (latestIndexByWatched.size === 0) return activities;
20530
+ return activities.map((activity, index) => {
20531
+ if (activity.kind !== "fusion.evidence.summary") return activity;
20532
+ const payload = asRecord$7(activity.payload);
20533
+ const watchedThreadId = payload?.watchedThreadId;
20534
+ if (!payload || typeof watchedThreadId !== "string" || latestIndexByWatched.get(watchedThreadId) === index || payload.summary === null || payload.summary === void 0) return activity;
20535
+ return {
20536
+ ...activity,
20537
+ payload: {
20538
+ ...payload,
20539
+ summary: null,
20540
+ summaryDropped: true
20541
+ }
20542
+ };
20543
+ });
20544
+ }
20408
20545
  function projectThreadDetailSnapshot(snapshot) {
20409
20546
  return {
20410
20547
  ...snapshot,
20411
20548
  thread: {
20412
20549
  ...snapshot.thread,
20413
- activities: dropSupersededToolUpdatedActivities(dropStaleContextWindowActivities(snapshot.thread.activities).map(projectActivityPayload))
20550
+ activities: dropSupersededEvidenceSummaries(dropSupersededToolUpdatedActivities(dropStaleContextWindowActivities(snapshot.thread.activities).map(projectActivityPayload)))
20414
20551
  }
20415
20552
  };
20416
20553
  }
@@ -33071,7 +33208,7 @@ const layer$87 = Layer.effect(AssetSync, make$103);
33071
33208
  * runtime template. Applies to assistant prose only: code blocks, commits,
33072
33209
  * PRs, error strings, and safety-critical text stay uncompressed.
33073
33210
  */
33074
- const COMPRESS_SHARED_RULES = `Respond terse. Keep all technical substance; remove fluff.
33211
+ const COMPRESS_SHARED_RULES = `Respond terse like smart caveman. All technical substance stay. Only fluff die.
33075
33212
 
33076
33213
  ## Persistence
33077
33214
 
@@ -41404,6 +41541,37 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
41404
41541
  detached_at AS "detachedAt"
41405
41542
  FROM thread_pairs
41406
41543
  ORDER BY created_at ASC, pair_id ASC
41544
+ `
41545
+ });
41546
+ const LatestContextWindowRow = Schema$1.Struct({
41547
+ usedTokens: Schema$1.Number,
41548
+ maxTokens: Schema$1.NullOr(Schema$1.Number)
41549
+ });
41550
+ const getLatestContextWindowRow = SqlSchema.findOneOption({
41551
+ Request: ThreadId,
41552
+ Result: LatestContextWindowRow,
41553
+ execute: (threadId) => sql`
41554
+ SELECT
41555
+ json_extract(payload_json, '$.usedTokens') AS "usedTokens",
41556
+ json_extract(payload_json, '$.maxTokens') AS "maxTokens"
41557
+ FROM projection_thread_activities
41558
+ WHERE thread_id = ${threadId}
41559
+ AND kind = 'context-window.updated'
41560
+ AND json_extract(payload_json, '$.usedTokens') >= 0
41561
+ ORDER BY created_at DESC, rowid DESC
41562
+ LIMIT 1
41563
+ `
41564
+ });
41565
+ const EvidenceReadContextRow = Schema$1.Struct({ usedTokensAtRead: Schema$1.Number });
41566
+ const getEvidenceReadContextRow = SqlSchema.findOneOption({
41567
+ Request: Schema$1.String,
41568
+ Result: EvidenceReadContextRow,
41569
+ execute: (activityId) => sql`
41570
+ SELECT json_extract(payload_json, '$.usedTokensAtRead') AS "usedTokensAtRead"
41571
+ FROM projection_thread_activities
41572
+ WHERE activity_id = ${activityId}
41573
+ AND json_extract(payload_json, '$.usedTokensAtRead') >= 0
41574
+ LIMIT 1
41407
41575
  `
41408
41576
  });
41409
41577
  const getThreadPairRow = SqlSchema.findOneOption({
@@ -43193,6 +43361,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
43193
43361
  messageCreatedAt: row.messageCreatedAt
43194
43362
  })) };
43195
43363
  });
43364
+ const getLatestThreadContextWindow = (threadId) => getLatestContextWindowRow(threadId).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getLatestThreadContextWindow:query", "ProjectionSnapshotQuery.getLatestThreadContextWindow:decodeRow")));
43365
+ const getEvidenceReadContext = (activityId) => getEvidenceReadContextRow(activityId).pipe(Effect.map(Option.map((row) => row.usedTokensAtRead)), Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getEvidenceReadContext:query", "ProjectionSnapshotQuery.getEvidenceReadContext:decodeRow")));
43196
43366
  const getThreadPairById = (pairId) => getThreadPairRow(pairId).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadPairById:query", "ProjectionSnapshotQuery.getThreadPairById:decodeRow")));
43197
43367
  const getThreadShellById = (threadId) => Effect.gen(function* () {
43198
43368
  const [threadRow, latestTurnRow, sessionRow] = yield* Effect.all([
@@ -43418,7 +43588,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
43418
43588
  getThreadDetailById,
43419
43589
  getHandoffContext,
43420
43590
  getBtwContext,
43421
- getThreadDetailSnapshot
43591
+ getThreadDetailSnapshot,
43592
+ getLatestThreadContextWindow,
43593
+ getEvidenceReadContext
43422
43594
  };
43423
43595
  });
43424
43596
  const OrchestrationProjectionSnapshotQueryLive = Layer.effect(ProjectionSnapshotQuery, makeProjectionSnapshotQuery).pipe(Layer.provide(layer$86));
@@ -43482,6 +43654,38 @@ function payloadPreview(payload) {
43482
43654
  truncated
43483
43655
  };
43484
43656
  }
43657
+ /**
43658
+ * Context-window rows are bookkeeping, not work.
43659
+ *
43660
+ * Every turn emits a stream of them and a single one can be large enough to
43661
+ * truncate, so leaving them in spends a reviewer's page budget on a token
43662
+ * meter they did not ask about. The prefix test covers the truncated case,
43663
+ * where the payload arrives as a JSON fragment that can no longer be parsed
43664
+ * but still carries the kind near its head.
43665
+ */
43666
+ const CONTEXT_WINDOW_ACTIVITY_MARKER = "\"kind\":\"context-window.updated\"";
43667
+ function isContextWindowBookkeeping(type, payload, sourceTruncated) {
43668
+ if (type !== "thread.activity-appended") return false;
43669
+ if (sourceTruncated) return typeof payload === "string" && payload.includes(CONTEXT_WINDOW_ACTIVITY_MARKER);
43670
+ const record = payload;
43671
+ return typeof record === "object" && record !== null && record.activity?.kind === "context-window.updated";
43672
+ }
43673
+ /**
43674
+ * The trace an attachment leaves on a merged message.
43675
+ *
43676
+ * Fragments of one message collapse into a single item by identity, so an
43677
+ * image or a document has nowhere else to appear. Name and size are what a
43678
+ * reviewer needs to ask for it at source; the bytes themselves never belong on
43679
+ * a review page.
43680
+ */
43681
+ function attachmentEvidence(attachments) {
43682
+ if (!attachments?.length) return void 0;
43683
+ return attachments.slice(0, 10).map((attachment) => ({
43684
+ type: attachment.type,
43685
+ name: attachment.name.slice(0, 120),
43686
+ sizeBytes: attachment.sizeBytes
43687
+ }));
43688
+ }
43485
43689
  function fitItem(item) {
43486
43690
  let result = item;
43487
43691
  let bytes = Buffer.byteLength(JSON.stringify(result));
@@ -43518,7 +43722,7 @@ function createThreadReviewPage(input) {
43518
43722
  }));
43519
43723
  function offer(event, sourceTruncated = false) {
43520
43724
  if (event.sequence > input.throughSequence) return false;
43521
- if (event.aggregateKind !== "thread" || event.aggregateId !== input.threadId) {
43725
+ if (event.aggregateKind !== "thread" || event.aggregateId !== input.threadId || isContextWindowBookkeeping(event.type, event.payload, sourceTruncated)) {
43522
43726
  nextAfterSequence = event.sequence;
43523
43727
  scannedEvents += 1;
43524
43728
  return true;
@@ -43526,7 +43730,7 @@ function createThreadReviewPage(input) {
43526
43730
  let item;
43527
43731
  let index;
43528
43732
  let messageKey;
43529
- if (event.type === "thread.message-sent" && !sourceTruncated && isMessagePayload(event.payload) && event.payload.role === "assistant" && !event.payload.attachments?.length && event.payload.messageId.length + (event.payload.turnId?.length ?? 0) <= MAX_MESSAGE_IDENTITY_LENGTH) {
43733
+ if (event.type === "thread.message-sent" && !sourceTruncated && isMessagePayload(event.payload) && event.payload.role === "assistant" && event.payload.messageId.length + (event.payload.turnId?.length ?? 0) <= MAX_MESSAGE_IDENTITY_LENGTH) {
43530
43734
  const payload = event.payload;
43531
43735
  messageKey = JSON.stringify([payload.turnId, payload.messageId]);
43532
43736
  index = messageIndexes.get(messageKey);
@@ -43534,6 +43738,7 @@ function createThreadReviewPage(input) {
43534
43738
  const previousMessage = previous?.kind === "message" ? previous : void 0;
43535
43739
  const replaces = !payload.streaming && payload.text.length > 0;
43536
43740
  const part = excerpt(payload.text, THREAD_REVIEW_ITEM_MAX_BYTES);
43741
+ const attachments = attachmentEvidence(payload.attachments) ?? previousMessage?.attachments;
43537
43742
  item = fitItem({
43538
43743
  kind: "message",
43539
43744
  firstSequence: previousMessage?.firstSequence ?? event.sequence,
@@ -43544,7 +43749,8 @@ function createThreadReviewPage(input) {
43544
43749
  operation: replaces ? "replace" : previousMessage?.operation ?? "append",
43545
43750
  complete: !payload.streaming,
43546
43751
  text: replaces ? part : (previousMessage?.text ?? "") + part,
43547
- truncated: part !== payload.text || !replaces && (previousMessage?.truncated ?? false)
43752
+ truncated: part !== payload.text || !replaces && (previousMessage?.truncated ?? false),
43753
+ ...attachments === void 0 ? {} : { attachments }
43548
43754
  });
43549
43755
  } else {
43550
43756
  const preview = sourceTruncated && typeof event.payload === "string" ? {
@@ -59043,14 +59249,14 @@ const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(functio
59043
59249
  * written as if the setting applied only to user-facing answers.
59044
59250
  */
59045
59251
  const FUSION_PAIR_COMMUNICATION_INSTRUCTIONS = `Everything you send the other half of the pair obeys the thread's active response-compression mode, exactly as a user-facing answer does: phase reports, advice, reviews, questions, and handoffs. When the mode is off, write normal prose. Compression governs form only - never drop a requirement, a negation, a number or unit, a file, symbol, or command name, code, security or warning text, the evidence behind a claim, or an exact protocol heading or token in order to save words.`;
59046
- const FUSION_BUILDER_INSTRUCTIONS = `You are Fusion Builder in an already-created native server pair. Server owns pairing and coordination. Follow the current turn's [fusion-review-policy] block when present; it overrides the phase-boundary scheduling rules below. Do not inspect or invoke the Fusion skill, create/pair/rename threads, or announce/setup Fusion. Start the user's task directly. Before editing, create and maintain the phase list with your provider's step-tracking tool (Claude Code: TaskCreate for each phase, then TaskUpdate for status, or TodoWrite when that is the tool offered; Codex: update_plan), never the MCP task board tools - one entry per phase in order, exactly one in progress at a time, marked completed at each phase end - so phases render in the task banner. That list holds phase entries only for the whole task; keep step-level or per-file todos out of it. Name each phase in 3-6 words by its outcome, never by a command, file path, or flag, because the banner shows the title verbatim. Prose alone leaves the banner empty. Split it into the fewest substantial phases the task genuinely needs plus a final integration/whole-task phase; most tasks need one to three work phases. Each phase is a complete reviewable slice of behavior. Never split per file, per function, or per trivial step: over-splitting spends review turns instead of finishing the job. Add a phase only when a real review boundary, risky decision, or independent behavior separates the work. Complete exactly one phase per turn, and finish the whole phase in that turn rather than stopping early. Do not run tests, typecheck, lint, or builds per phase; write the tests the change needs, then run verification once in the final phase over the whole task. Exception: a phase whose own correctness is unclear may run the single narrowest check that resolves it. End every phase turn with phase completed, todo status, changed behavior/files, and remaining phases; do not start the next phase in the same turn. Server then wakes the paired Supervisor, which resumes you through ${FUSION_ADVICE_PROMPT_PREFIX}; a user message may also revise or resume the work. Final phase verifies the entire task against the original request and labels it ready for whole-task review. Supervisor is unreachable during your turn. Never spawn/use another Supervisor thread/subagent or attribute Supervisor decisions without ${FUSION_ADVICE_PROMPT_PREFIX}. Within the current phase, continue when straightforward or evidence is clear. For a concrete unresolved tradeoff, correctness risk, or design decision materially needing judgment, stop safely before the risky choice; final response states the exact question and why review is needed. Evaluate/follow Supervisor advice unless conflicting with user request or verified repo state. ${FUSION_PAIR_COMMUNICATION_INSTRUCTIONS}`;
59047
- const FUSION_WATCHER_TOOL_INSTRUCTIONS = `Supervisor tools belong to the p4-code MCP server: thread_watch_review, thread_watch_events, thread_advise, and thread_gate_respond. If absent from the visible tool list, first use the available tool search/discovery facility to load those exact tools, including provider-prefixed names; absence from the initial list does not establish unavailability. If discovery or a call fails, report the exact missing capability or error and the need to restore the p4-code MCP connection. Do not offer manual relay, draft or implement the task yourself, or abandon the supervisor role as a workaround. For an actionable user requirement, read the relevant builder evidence and relay it through thread_advise; when a gate is open, answer through thread_gate_respond instead. Structure every actionable supervisor advice message with these Markdown headings, in order:
59252
+ const FUSION_BUILDER_INSTRUCTIONS = `You are Fusion Builder in an already-created native server pair. Server owns pairing and coordination. Follow the current turn's [fusion-review-policy] block when present; it overrides the phase-boundary scheduling rules below. Do not inspect or invoke the Fusion skill, create/pair/rename threads, or announce/setup Fusion. Start the user's task directly. The phase plan is not yours: Supervisor authors it, orders it, and moves every phase status, and it renders in your task banner from there. Never create or edit a phase list yourself - not with TaskCreate, TaskUpdate, TodoWrite, update_plan, the MCP task board tools, or thread_plan_update - and never mark your own phase complete. Work the one phase Supervisor put in progress, and report what you did. Step-level notes for your own use are fine as long as they never become a phase list. Complete exactly one phase per turn, and finish the whole phase in that turn rather than stopping early. Do not run tests, typecheck, lint, or builds per phase; write the tests the change needs, then run verification once in the final phase over the whole task. Exception: a phase whose own correctness is unclear may run the single narrowest check that resolves it. End every phase turn with a report of at most six non-empty lines: the phase you finished, what changed and where, what you verified with its result, and anything unresolved. One line each, every fact once, no restatement of the plan or of advice you were given. Code, exact error text and security warnings are exempt from the cap. Do not start the next phase in the same turn. Server then wakes the paired Supervisor, which resumes you through ${FUSION_ADVICE_PROMPT_PREFIX}; a user message may also revise or resume the work. Final phase verifies the entire task against the original request and labels it ready for whole-task review. Supervisor is unreachable during your turn. Never spawn/use another Supervisor thread/subagent or attribute Supervisor decisions without ${FUSION_ADVICE_PROMPT_PREFIX}. Within the current phase, continue when straightforward or evidence is clear. For a concrete unresolved tradeoff, correctness risk, or design decision materially needing judgment, stop safely before the risky choice; final response states the exact question and why review is needed. Evaluate/follow Supervisor advice unless conflicting with user request or verified repo state. ${FUSION_PAIR_COMMUNICATION_INSTRUCTIONS}`;
59253
+ const FUSION_WATCHER_TOOL_INSTRUCTIONS = `Supervisor tools belong to the p4-code MCP server: thread_watch_review, thread_watch_events, thread_evidence_report, thread_plan_update, thread_advise, and thread_gate_respond. If absent from the visible tool list, first use the available tool search/discovery facility to load those exact tools, including provider-prefixed names; absence from the initial list does not establish unavailability. If discovery or a call fails, report the exact missing capability or error and the need to restore the p4-code MCP connection. Do not offer manual relay, draft or implement the task yourself, or abandon the supervisor role as a workaround. For an actionable user requirement, read the relevant builder evidence and relay it through thread_advise; when a gate is open, answer through thread_gate_respond instead. Structure every actionable supervisor advice message with these Markdown headings, in order:
59048
59254
  ## Recommendation
59049
59255
  State the decision or correction in one sentence. For a phase review, say whether the phase is approved or needs changes.
59050
59256
  ## Reason
59051
59257
  Give concise evidence, relevant file/line references, and any unresolved risk. Distinguish observed facts from assumptions; do not claim checks you did not run. Before prescribing a blocking product fix, identify the violated requirement and evidence of product ownership. If ownership is uncertain, state the hypothesis and request the smallest discriminating check, such as comparing the unchanged baseline or another app, rather than prescribing a speculative fix. Do not block a product fix on an artifact reproduced outside the product unless evidence still ties it to an unmet task requirement.
59052
59258
  ## Next step
59053
- Give ordered, concrete actions for the builder, including the next phase and its completion condition. Preserve user requirements, constraints, and delivery authorization. Before repeating an objection, compare the builder's response and intervening changes with the prior finding; cite new evidence and explain why the builder's response does not resolve it. Without new evidence and an actionable next step, do not resend the objection or repeat passed checks; report the unresolved state in your own thread and end. A supported unresolved blocker remains unapproved; avoiding repetition never justifies approval. For an open gate, follow its response and escalation protocol. Keep each section short; omit empty bullets and repeated status. Formatting does not authorize an otherwise unnecessary advice turn or replace an exact protocol response.`;
59259
+ Give ordered, concrete actions for the builder, including the next phase and its completion condition. Preserve user requirements, constraints, and delivery authorization. Before repeating an objection, compare the builder's response and intervening changes with the prior finding; cite new evidence and explain why the builder's response does not resolve it. Without new evidence and an actionable next step, do not resend the objection or repeat passed checks; report the unresolved state in your own thread and end. A supported unresolved blocker remains unapproved; avoiding repetition never justifies approval. For an open gate, follow its response and escalation protocol. One line per section is the target and three is the ceiling; omit empty bullets and repeated status. Quote only the evidence the decision rests on, once, with its file:line or sequence. Exact errors, code and security text are exempt. Formatting does not authorize an otherwise unnecessary advice turn or replace an exact protocol response.`;
59054
59260
  /**
59055
59261
  * The directive that makes the supervisor read evidence through a subagent.
59056
59262
  *
@@ -59058,8 +59264,8 @@ Give ordered, concrete actions for the builder, including the next phase and its
59058
59264
  * turn off: with `Review evidence` off the supervisor reads the range itself,
59059
59265
  * which is what it did before this existed.
59060
59266
  */
59061
- const FUSION_EVIDENCE_SUMMARY_INSTRUCTIONS = `Read the builder's evidence through a summarizer subagent rather than into your own context: spawn one subagent with the evidence range and have it call thread_watch_review itself, paging with nextAfterSequence against a fixed throughSequence until hasMore is false, and return a summary. Your tool grants reach it, so it can read the paired builder thread exactly as you can. Instruct that subagent that thread_watch_review and thread_watch_events may be absent from its visible tool list and that absence does not establish unavailability: it must first use the available tool search/discovery facility to load those exact tools by name, including provider-prefixed names, and if discovery itself fails it must report that exact error rather than reporting no such tool. Require of that summary: every file path touched with the scale of the change to each, every command run and whether it succeeded or failed, check and test results with their counts, exact error text quoted rather than characterized, the builder's completion claims marked as claims, and anything the builder flagged as uncertain, blocked or unresolved. Over a multi-turn range it must also carry unresolved objections of yours and whether each was answered, phases completed versus reopened, claims never verified, and files touched repeatedly. Every statement cites the sequences behind it so you can check any one of them with a targeted thread_watch_events read. Fail open: if no subagent is available, the spawn errors, it times out, or what comes back is empty or does not cite sequences, read the range yourself and review from the raw evidence. Never skip or shorten a review because a summary was unavailable. Record which fallback you took: a subagent that could not load the watch tools is a misconfiguration worth naming in your report, and it is not the same as a spawn that errored or timed out.`;
59062
- const FUSION_WATCHER_INSTRUCTIONS = `You are Fusion Supervisor (watcher) in an already-created native server pair. Follow the current turn's [fusion-review-policy] block when present; it overrides phase scheduling. Server owns pairing and coordination and wakes you with ${FUSION_REVIEW_PROMPT_PREFIX} or ${FUSION_GATE_PROMPT_PREFIX} prompts at builder turn boundaries. A plain message outside such a wake may arrive after your conversational memory of the pair is gone; its [fusion-pair] metadata block is authoritative: the builder thread exists and is the counterpart thread id. Never report that no builder thread exists. For a new pair, the initial user request is yours, and its first turn is research, not relay. Research it: when the request names a ticket, read that ticket AND its comments, including replies and inline comments, and treat a later comment as newer intent than the description; read the code, docs, project instructions, and current state the request touches. Analyse what you found: root cause or the concrete design constraint, scope, what the user actually wants delivered, and which delivery steps they authorized. Only then direct the first actionable phase to the idle builder through thread_advise, stating the findings that make the direction actionable alongside the original requirements, constraints, and authorized delivery scope. Never echo the request back as its own direction, never hand over a plan you did not ground in evidence, and never send ${FUSION_NO_OBJECTION_TEXT} before the builder has received its first phase and produced a turn to review. The builder has not received the initial prompt and must not start before your direction. Missing builder turn evidence is expected before that first direction; do not wait for it or implement the work yourself. Ask the user only when needed to resolve a blocking requirement. ${FUSION_WATCHER_TOOL_INSTRUCTIONS} To resume supervision, read builder evidence with thread_watch_review from lastReviewedImplementerSequence, capture its throughSequence on the first page and reuse that fixed bound while paging with nextAfterSequence until hasMore is false (including empty pages). Apply message append/replace operations by identity; recover truncated evidence through targeted raw thread_watch_events source ranges. Retain reviewed requirements and evidence for final review; recover missing context with targeted history reads rather than mandatory raw replay from zero, derive phase from artifacts (git log/status, PR, builder events, including its turn.plan.updated phase list), steer with thread_advise, and answer an open gate with thread_gate_respond. When a review or gate wake prompt specifies an explicit event range, that range wins over this metadata. Never poll or wait for the builder; deliver review or advice, then end the turn. Every thread_advise starts a builder turn whose completion wakes you again, so never advise a builder that is idle on an external wait or has nothing actionable; report the state in your own thread and end without advising. ${FUSION_PAIR_COMMUNICATION_INSTRUCTIONS}`;
59267
+ const FUSION_EVIDENCE_SUMMARY_INSTRUCTIONS = `Read the builder's evidence through a summarizer subagent rather than into your own context: spawn one subagent with the evidence range and have it call thread_watch_review itself, paging with nextAfterSequence against a fixed throughSequence until hasMore is false, and return a summary. Your tool grants reach it, so it can read the paired builder thread exactly as you can. Instruct that subagent that thread_watch_review and thread_watch_events may be absent from its visible tool list and that absence does not establish unavailability: it must first use the available tool search/discovery facility to load those exact tools by name, including provider-prefixed names, and if discovery itself fails it must report that exact error rather than reporting no such tool. Require of that summary: every file path touched with the scale of the change to each, every command run and whether it succeeded or failed, check and test results with their counts, exact error text quoted rather than characterized, the builder's completion claims marked as claims, and anything the builder flagged as uncertain, blocked or unresolved. Over a multi-turn range it must also carry unresolved objections of yours and whether each was answered, phases completed versus reopened, claims never verified, and files touched repeatedly. Every statement cites the sequences behind it so you can check any one of them with a targeted thread_watch_events read. Fail open: if no subagent is available, the spawn errors, it times out, or what comes back is empty or does not cite sequences, read the range yourself and review from the raw evidence. Never skip or shorten a review because a summary was unavailable. Record which fallback you took: a subagent that could not load the watch tools is a misconfiguration worth naming in your report, and it is not the same as a spawn that errored or timed out. Then close the review by calling thread_evidence_report exactly once, after the last page: pass the watched thread id, the same fixed throughSequence the first review page returned - not lastSequence and not the final page's nextAfterSequence, because the server pairs the report to the read by that exact number - the first and last sequence you actually covered, the model you spawned on, and outcome summarized with the subagent's exact returned text, fallback when you read the raw range yourself, or failed when the summarizer could not run or could not report - with the reason in your own words for the last two. Report even when the summary never arrived; a review with no report cannot be told apart from a supervisor that never tried. Never paste builder tool output into it.`;
59268
+ const FUSION_WATCHER_INSTRUCTIONS = `You are Fusion Supervisor (watcher) in an already-created native server pair. Follow the current turn's [fusion-review-policy] block when present; it overrides phase scheduling. Server owns pairing and coordination and wakes you with ${FUSION_REVIEW_PROMPT_PREFIX} or ${FUSION_GATE_PROMPT_PREFIX} prompts at builder turn boundaries. A plain message outside such a wake may arrive after your conversational memory of the pair is gone; its [fusion-pair] metadata block is authoritative: the builder thread exists and is the counterpart thread id. Never report that no builder thread exists. For a new pair, the initial user request is yours, and its first turn is research, not relay. Research it: when the request names a ticket, read that ticket AND its comments, including replies and inline comments, and treat a later comment as newer intent than the description; read the code, docs, project instructions, and current state the request touches. Analyse what you found: root cause or the concrete design constraint, scope, what the user actually wants delivered, and which delivery steps they authorized. Only then write the phase plan and direct its first phase. The plan is yours alone: call thread_plan_update with the builder's threadId, sending the complete ordered list every time, each phase named in 3-6 words by its outcome rather than by a command, file path, or flag, with exactly one in progress. Split it into the fewest substantial phases the task genuinely needs plus a final integration/whole-task phase; most tasks need one to three work phases, each a complete reviewable slice of behavior. Never split per file, per function, or per trivial step. State each phase's completion condition to the builder in the advice, not in the banner title. On every later review, rewrite the same plan: an approved phase becomes completed and the next becomes in progress in the same call, an objection leaves the current phase in progress or reopens a phase you had marked completed, and the final approval completes the last phase. The builder never touches it, so a phase you do not move stays where it is. Then direct the phase to the idle builder through thread_advise, stating the findings that make the direction actionable alongside the original requirements, constraints, and authorized delivery scope. Never echo the request back as its own direction, never hand over a plan you did not ground in evidence, and never send ${FUSION_NO_OBJECTION_TEXT} before the builder has received its first phase and produced a turn to review. The builder has not received the initial prompt and must not start before your direction. Missing builder turn evidence is expected before that first direction; do not wait for it or implement the work yourself. Ask the user only when needed to resolve a blocking requirement. ${FUSION_WATCHER_TOOL_INSTRUCTIONS} To resume supervision, read builder evidence with thread_watch_review from lastReviewedImplementerSequence, capture its throughSequence on the first page and reuse that fixed bound while paging with nextAfterSequence until hasMore is false (including empty pages). Apply message append/replace operations by identity; recover truncated evidence through targeted raw thread_watch_events source ranges. Retain reviewed requirements and evidence for final review; recover missing context with targeted history reads rather than mandatory raw replay from zero, derive phase from artifacts (git log/status, PR, builder events, including its turn.plan.updated phase list), steer with thread_advise, and answer an open gate with thread_gate_respond. When a review or gate wake prompt specifies an explicit event range, that range wins over this metadata. Never poll or wait for the builder; deliver review or advice, then end the turn. Every thread_advise starts a builder turn whose completion wakes you again, so never advise a builder that is idle on an external wait or has nothing actionable; report the state in your own thread and end without advising. ${FUSION_PAIR_COMMUNICATION_INSTRUCTIONS}`;
59063
59269
  /**
59064
59270
  * The one-line stand-in for the full block on a message whose session already
59065
59271
  * carries the role instructions.
@@ -59119,10 +59325,20 @@ function resolveFusionSummarizerSelection(settings) {
59119
59325
  function summarizerModelLine(model) {
59120
59326
  return model === null ? "No summarizer model is configured for this provider, so read the range yourself." : `Spawn that subagent on ${model}, a cheap fast model of your own provider.`;
59121
59327
  }
59328
+ /**
59329
+ * What a pair is told when the settings file cannot be read.
59330
+ *
59331
+ * Every field here has to match the shipped default of the setting it stands
59332
+ * in for, or an unreadable settings file silently changes what a pair is told -
59333
+ * which is the one thing the fallback exists to prevent. `evidenceSummary`
59334
+ * tracks `fusionEvidenceSummary` in `packages/contracts/src/settings.ts`, whose
59335
+ * decoding default is on; a test asserts the two stay equal.
59336
+ */
59122
59337
  const DEFAULT_FUSION_PROMPT_SETTINGS = {
59123
59338
  promptOverrides: null,
59124
59339
  summarizerModel: null,
59125
- evidenceSummary: false
59340
+ evidenceSummary: true,
59341
+ optimizedContext: DEFAULT_SERVER_SETTINGS.enableOptimizedFusionPromptDelivery
59126
59342
  };
59127
59343
  /**
59128
59344
  * @param options.promptOverrides - The user's settings. An override replaces
@@ -59153,10 +59369,165 @@ function fusionRoleInstructionsWith(role, provider, settings) {
59153
59369
  ...provider === void 0 ? {} : { provider },
59154
59370
  summarizerModelOverride: settings?.summarizerModel ?? null,
59155
59371
  promptOverrides: settings?.promptOverrides ?? null,
59156
- evidenceSummary: settings?.evidenceSummary ?? false
59372
+ evidenceSummary: settings?.evidenceSummary ?? DEFAULT_FUSION_PROMPT_SETTINGS.evidenceSummary
59157
59373
  });
59158
59374
  }
59159
59375
  //#endregion
59376
+ //#region src/provider/FusionContextPolicy.ts
59377
+ /**
59378
+ * What a Fusion role is allowed to carry into its context.
59379
+ *
59380
+ * A pair pays for its context twice: once in the builder's window and once in
59381
+ * the supervisor's, on every turn of a review loop. Measured on a live pair,
59382
+ * the builder's session opened with 107,320 tokens of MCP tool definitions, of
59383
+ * which 86,005 belonged to servers neither role can use, plus 13,270 tokens of
59384
+ * global memory files. None of that is the user's task.
59385
+ *
59386
+ * The policy is provider-neutral on purpose: it says what a role may carry,
59387
+ * and each adapter expresses it with whatever native control it has. An
59388
+ * adapter that cannot express a rule keeps the baseline rather than pretending,
59389
+ * which is why the exported helpers are pure and the adapters stay the only
59390
+ * place that knows how a given CLI is configured.
59391
+ *
59392
+ * All of it is gated on the user's existing Fusion context optimization
59393
+ * setting, which is therefore the rollback: off reproduces today's context
59394
+ * byte for byte, for both roles and for every ordinary thread.
59395
+ *
59396
+ * @module provider/FusionContextPolicy
59397
+ */
59398
+ /**
59399
+ * The four constraints that must survive losing global memory.
59400
+ *
59401
+ * Deliberately not a copy of the user's `CLAUDE.md`: that file is thousands of
59402
+ * tokens of preference, and only these four are things a wrong answer cannot be
59403
+ * walked back from. Everything else a pair needs is in the project's own
59404
+ * instructions, which this policy never drops.
59405
+ */
59406
+ const FUSION_SAFETY_KERNEL = [
59407
+ "Safety rules for this session, which override any default behaviour:",
59408
+ "- Never commit secrets or .env files, and never delete a file without explicit confirmation in the current turn.",
59409
+ "- Only touch work this session created. Never revert, stash, or overwrite uncommitted changes belonging to another session, and stage explicit paths rather than everything.",
59410
+ "- Never commit, push, or open a pull request unless the current turn asked for it, and never commit to main or master directly.",
59411
+ "- Never add AI attribution to a commit message, pull request, issue, comment, code, or document."
59412
+ ].join("\n");
59413
+ /** Everything the supervisor half needs from the P4 MCP server, and nothing else. */
59414
+ const FUSION_SUPERVISOR_P4_TOOLS = /* @__PURE__ */ new Set([
59415
+ "thread_watch_review",
59416
+ "thread_watch_events",
59417
+ "thread_evidence_report",
59418
+ "thread_advise",
59419
+ "thread_gate_respond",
59420
+ "thread_plan_update",
59421
+ "ask_user_question",
59422
+ "task_current"
59423
+ ]);
59424
+ /**
59425
+ * What the builder half must not reach.
59426
+ *
59427
+ * Framed as a denial rather than an allowance because the builder's job is
59428
+ * open-ended: it needs coding, verification and preview tools, and a new one of
59429
+ * those should reach it without an edit here. What it must never have is the
59430
+ * pair's own machinery - spawning threads, creating or detaching pairs,
59431
+ * advising, answering its own gate - the phase plan, which the supervisor
59432
+ * owns outright, plus task-board mutation and the external services that
59433
+ * belong to a person rather than to an implementation turn.
59434
+ */
59435
+ const FUSION_BUILDER_DENIED_P4_TOOLS = /* @__PURE__ */ new Set([
59436
+ "thread_spawn",
59437
+ "thread_pair_create",
59438
+ "thread_pair_detach",
59439
+ "thread_cleanup",
59440
+ "thread_rename",
59441
+ "thread_configure",
59442
+ "thread_snooze",
59443
+ "thread_settle",
59444
+ "thread_advise",
59445
+ "thread_gate_respond",
59446
+ "thread_plan_update",
59447
+ "task_create",
59448
+ "task_update",
59449
+ "task_propose",
59450
+ "memory_append",
59451
+ "ticket_resolve"
59452
+ ]);
59453
+ /**
59454
+ * Native planning tools the builder must not see either.
59455
+ *
59456
+ * The MCP denial above is only half of it: every provider ships its own
59457
+ * step-tracking tool, and a builder holding one would keep authoring the plan
59458
+ * the supervisor now owns. Named per provider because the names are the
59459
+ * provider's, not ours.
59460
+ */
59461
+ const FUSION_BUILDER_DENIED_NATIVE_TOOLS = [
59462
+ "TaskCreate",
59463
+ "TaskUpdate",
59464
+ "TaskGet",
59465
+ "TaskList",
59466
+ "TodoWrite",
59467
+ "update_plan"
59468
+ ];
59469
+ /**
59470
+ * What an adapter assumes when no optimization resolver was supplied.
59471
+ *
59472
+ * Mirrors the shipped setting rather than defaulting to off, so a driver that
59473
+ * has not been wired yet does not silently disable the profile.
59474
+ */
59475
+ const DEFAULT_FUSION_CONTEXT_OPTIMIZATION = DEFAULT_SERVER_SETTINGS.enableOptimizedFusionPromptDelivery;
59476
+ /** The policy input, for adapters that read the two values separately. */
59477
+ function fusionContextPolicyFor(role, optimizedContext) {
59478
+ return {
59479
+ role,
59480
+ optimizedContext
59481
+ };
59482
+ }
59483
+ /** Whether the lean profile applies at all. Ordinary threads never reach it. */
59484
+ function fusionLeanContextApplies(input) {
59485
+ return input.role !== void 0 && input.optimizedContext;
59486
+ }
59487
+ /**
59488
+ * The external MCP registrations a Fusion session keeps: none.
59489
+ *
59490
+ * Everything a user registered for their own work goes, because neither role
59491
+ * can act on it during a pair turn and every definition is paid for on every
59492
+ * turn of the loop. Computer Use needs no exception here - it is a
59493
+ * provider-owned endpoint attached only to a session that was granted it, not
59494
+ * a registration in this map, so a granted session keeps it either way.
59495
+ */
59496
+ function fusionExternalMcpServers(servers, input) {
59497
+ return fusionLeanContextApplies(input) ? {} : { ...servers };
59498
+ }
59499
+ /**
59500
+ * P4 tools this role must not see, given the full catalogue.
59501
+ *
59502
+ * Takes the catalogue rather than hard-coding it so a tool added to the server
59503
+ * is denied to the supervisor by default and offered to the builder by
59504
+ * default, which is the safe direction for each.
59505
+ */
59506
+ function fusionHiddenP4Tools(catalogue, input) {
59507
+ if (!fusionLeanContextApplies(input)) return [];
59508
+ return input.role === "watcher" ? catalogue.filter((name) => !FUSION_SUPERVISOR_P4_TOOLS.has(name)) : catalogue.filter((name) => FUSION_BUILDER_DENIED_P4_TOOLS.has(name));
59509
+ }
59510
+ /**
59511
+ * Provider-native tools this role must not see.
59512
+ *
59513
+ * The supervisor keeps its own planning tool: it is the half that plans, and
59514
+ * for its own thread that tool is the ordinary one.
59515
+ */
59516
+ function fusionHiddenNativeTools(input) {
59517
+ return fusionLeanContextApplies(input) && input.role === "implementer" ? FUSION_BUILDER_DENIED_NATIVE_TOOLS : [];
59518
+ }
59519
+ /**
59520
+ * Setting sources a Fusion session reads.
59521
+ *
59522
+ * `user` carries the global memory files and the user-level skills; the
59523
+ * project's own instructions live in `project` and `local` and stay, because
59524
+ * they are the ones that describe the code being changed.
59525
+ */
59526
+ function fusionSettingSources(sources, input) {
59527
+ if (!fusionLeanContextApplies(input)) return sources;
59528
+ return sources.filter((source) => source !== "user");
59529
+ }
59530
+ //#endregion
59160
59531
  //#region src/provider/ProviderOwnedProcessRegistry.ts
59161
59532
  /**
59162
59533
  * ProviderOwnedProcessRegistry - threadId to owned provider process handles.
@@ -61590,6 +61961,67 @@ function providerMcpEndpoints(config) {
61590
61961
  authorizationHeader: config.computerUse.authorizationHeader
61591
61962
  }] : []];
61592
61963
  }
61964
+ /**
61965
+ * Every tool the built-in `p4-code` server registers, by bare name.
61966
+ *
61967
+ * Declared rather than derived so an adapter can filter a session's tools
61968
+ * without importing the MCP server layer, which would drag the whole
61969
+ * orchestration graph into a provider process. `McpHttpServer.test.ts` asserts
61970
+ * this list equals what the toolkits actually register, so a tool added there
61971
+ * cannot silently go missing here.
61972
+ */
61973
+ const P4CODE_MCP_TOOL_NAMES = [
61974
+ "ask_user_question",
61975
+ "asset_compress",
61976
+ "device_action",
61977
+ "device_close",
61978
+ "device_command",
61979
+ "device_inspect",
61980
+ "device_list",
61981
+ "device_open",
61982
+ "device_screenshot",
61983
+ "memory_append",
61984
+ "preview_click",
61985
+ "preview_evaluate",
61986
+ "preview_navigate",
61987
+ "preview_open",
61988
+ "preview_press",
61989
+ "preview_recording_start",
61990
+ "preview_recording_stop",
61991
+ "preview_resize",
61992
+ "preview_save_screenshot",
61993
+ "preview_scroll",
61994
+ "preview_set_appearance",
61995
+ "preview_snapshot",
61996
+ "preview_status",
61997
+ "preview_type",
61998
+ "preview_wait_for",
61999
+ "task_create",
62000
+ "task_current",
62001
+ "task_get",
62002
+ "task_list",
62003
+ "task_propose",
62004
+ "task_update",
62005
+ "thread_advise",
62006
+ "thread_cleanup",
62007
+ "thread_configure",
62008
+ "thread_evidence_report",
62009
+ "thread_gate_respond",
62010
+ "thread_pair_create",
62011
+ "thread_pair_detach",
62012
+ "thread_plan_update",
62013
+ "thread_rename",
62014
+ "thread_settle",
62015
+ "thread_snooze",
62016
+ "thread_spawn",
62017
+ "thread_watch_events",
62018
+ "thread_watch_review",
62019
+ "ticket_resolve"
62020
+ ];
62021
+ /** The catalogue as the adapters read it. */
62022
+ function providerMcpToolNames() {
62023
+ return P4CODE_MCP_TOOL_NAMES;
62024
+ }
61593
62025
  /** Non-Codex identities stay reserved even when their endpoint is disabled. */
61594
62026
  function providerMcpReservedNames() {
61595
62027
  return /* @__PURE__ */ new Set([P4CODE_MCP_SERVER_NAME, COMPUTER_USE_MCP_SERVER_NAME]);
@@ -62279,7 +62711,7 @@ const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(function* (se
62279
62711
  },
62280
62712
  clientFileSystem: true,
62281
62713
  ...Option.isSome(cursor) ? { resumeSessionId: cursor.value.sessionId } : {},
62282
- mcpServers: antigravitySessionMcpServers(mcp, options.resolveMcpServers ? yield* options.resolveMcpServers : {}),
62714
+ mcpServers: antigravitySessionMcpServers(mcp, fusionExternalMcpServers(options.resolveMcpServers ? yield* options.resolveMcpServers : {}, fusionContextPolicyFor(input.fusionRole, options.resolveFusionContextOptimization === void 0 ? DEFAULT_FUSION_CONTEXT_OPTIMIZATION : yield* options.resolveFusionContextOptimization))),
62283
62715
  ...makeNativeLoggers({
62284
62716
  nativeEventLogger: options.nativeEventLogger,
62285
62717
  provider: PROVIDER$7,
@@ -64621,6 +65053,7 @@ const AntigravityDriver = {
64621
65053
  continuation: { groupKey: continuationIdentity.continuationKey }
64622
65054
  });
64623
65055
  const mcpRegistry = yield* McpRegistry;
65056
+ const serverSettings = yield* ServerSettingsService;
64624
65057
  const classifyModels = (draft) => modelManifest.current.pipe(Effect.map((manifest) => stampIdentity(applyModelManifest(draft, manifest, DRIVER))));
64625
65058
  const makeRuntime = Effect.fn("AntigravityDriver.makeRuntime")(function* (input) {
64626
65059
  if (authConfigIssue !== null) return yield* new ProviderSetupError({
@@ -64734,6 +65167,7 @@ const AntigravityDriver = {
64734
65167
  makeRuntime,
64735
65168
  withProcess: authFlow.withProcess,
64736
65169
  resolveMcpServers: mcpRegistry.resolveForSession,
65170
+ resolveFusionContextOptimization: serverSettings.getSettings.pipe(Effect.map((settings) => settings.enableOptimizedFusionPromptDelivery), Effect.orElseSucceed(() => DEFAULT_FUSION_CONTEXT_OPTIMIZATION)),
64737
65171
  defaultModel,
64738
65172
  onSessionStarted: provider.onSessionStarted,
64739
65173
  onConfigOptionsUpdated: provider.onConfigOptionsUpdated,
@@ -69266,13 +69700,19 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
69266
69700
  ...skillOverrides ? { skillOverrides } : {}
69267
69701
  };
69268
69702
  const mcpSession = readMcpProviderSession(input.threadId);
69269
- const externalMcpServers = Object.fromEntries(Object.entries(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers(input.cwd)).filter(([name]) => !providerMcpReservedNames().has(name)));
69703
+ const registeredMcpServers = Object.fromEntries(Object.entries(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers(input.cwd)).filter(([name]) => !providerMcpReservedNames().has(name)));
69270
69704
  const narrateBeforeTools = options?.resolveToolCallNarration === void 0 ? DEFAULT_SERVER_SETTINGS.enableToolCallNarration : yield* options.resolveToolCallNarration;
69271
69705
  const guardrailSettings = options?.resolveGuardrailPrompts === void 0 ? {
69272
69706
  enableVerificationBeforeCompletion: DEFAULT_SERVER_SETTINGS.enableVerificationBeforeCompletion,
69273
69707
  enableRootCauseBeforeFix: DEFAULT_SERVER_SETTINGS.enableRootCauseBeforeFix
69274
69708
  } : yield* options.resolveGuardrailPrompts;
69275
69709
  const fusionPromptSettings = options?.resolveFusionPromptSettings === void 0 ? void 0 : yield* options.resolveFusionPromptSettings;
69710
+ const fusionContextPolicy = {
69711
+ role: input.fusionRole,
69712
+ optimizedContext: fusionPromptSettings?.optimizedContext ?? DEFAULT_FUSION_PROMPT_SETTINGS.optimizedContext
69713
+ };
69714
+ const externalMcpServers = fusionExternalMcpServers(registeredMcpServers, fusionContextPolicy);
69715
+ const hiddenTools = [...fusionHiddenP4Tools(providerMcpToolNames(), fusionContextPolicy).map((name) => `mcp__${P4CODE_MCP_SERVER_NAME}__${name}`), ...fusionHiddenNativeTools(fusionContextPolicy)];
69276
69716
  const unpromptedSubagents = input.unpromptedSubagents !== void 0 ? input.unpromptedSubagents : options?.resolveUnpromptedSubagents === void 0 ? DEFAULT_SERVER_SETTINGS.enableUnpromptedSubagents : yield* options.resolveUnpromptedSubagents;
69277
69717
  const compressRuleset = compressRulesetFor(input.compressMode ?? "off");
69278
69718
  const systemPromptAppend = [
@@ -69281,8 +69721,9 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
69281
69721
  ...narrateBeforeTools ? [NARRATE_BEFORE_TOOLS_PROMPT] : [],
69282
69722
  ...guardrailPromptsFor(guardrailSettings),
69283
69723
  unpromptedSubagents ? SUBAGENTS_ALLOWED_PROMPT : SUBAGENTS_ON_REQUEST_PROMPT,
69284
- ...compressRuleset !== void 0 ? [compressRuleset] : [],
69285
- ...input.fusionRole !== void 0 ? [fusionRoleInstructionsWith(input.fusionRole, "claudeAgent", fusionPromptSettings)] : []
69724
+ ...input.fusionRole !== void 0 ? [fusionRoleInstructionsWith(input.fusionRole, "claudeAgent", fusionPromptSettings)] : [],
69725
+ ...fusionLeanContextApplies(fusionContextPolicy) ? [FUSION_SAFETY_KERNEL] : [],
69726
+ ...compressRuleset !== void 0 ? [compressRuleset] : []
69286
69727
  ].join("\n\n");
69287
69728
  const compressionSubagentHook = async (hookInput) => {
69288
69729
  if (hookInput.hook_event_name !== "SubagentStart") return {};
@@ -69301,7 +69742,8 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
69301
69742
  preset: "claude_code",
69302
69743
  ...systemPromptAppend.length > 0 ? { append: systemPromptAppend } : {}
69303
69744
  },
69304
- settingSources: [...CLAUDE_SETTING_SOURCES],
69745
+ settingSources: [...fusionSettingSources(CLAUDE_SETTING_SOURCES, fusionContextPolicy)],
69746
+ ...hiddenTools.length > 0 ? { disallowedTools: hiddenTools } : {},
69305
69747
  ...effectiveEffort ? { effort: effectiveEffort } : {},
69306
69748
  ...permissionMode ? { permissionMode } : {},
69307
69749
  ...permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
@@ -69710,7 +70152,8 @@ const ClaudeDriver = {
69710
70152
  resolveFusionPromptSettings: serverSettings.getSettings.pipe(Effect.map((settings) => ({
69711
70153
  promptOverrides: settings.fusionPromptOverrides,
69712
70154
  summarizerModel: resolveFusionSummarizerSelection(settings),
69713
- evidenceSummary: settings.fusionEvidenceSummary
70155
+ evidenceSummary: settings.fusionEvidenceSummary,
70156
+ optimizedContext: settings.enableOptimizedFusionPromptDelivery
69714
70157
  })), Effect.orElseSucceed(() => DEFAULT_FUSION_PROMPT_SETTINGS)),
69715
70158
  resolveUnpromptedSubagents: serverSettings.getSettings.pipe(Effect.map((settings) => settings.enableUnpromptedSubagents), Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS.enableUnpromptedSubagents)),
69716
70159
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
@@ -89079,8 +89522,11 @@ function buildCodexDeveloperInstructions(interactionMode, runtime, compressMode,
89079
89522
  return [
89080
89523
  browserToolsAvailable ? base : base.replace(P4_CODE_BROWSER_TOOL_INSTRUCTIONS, ""),
89081
89524
  ...guardrailPromptsFor(guardrailSettings),
89525
+ ...fusionRole === void 0 ? [] : [`<fusion_role>${fusionRoleInstructionsWith(fusionRole, "codex", fusionPromptSettings)}</fusion_role>`, ...fusionLeanContextApplies({
89526
+ role: fusionRole,
89527
+ optimizedContext: fusionPromptSettings?.optimizedContext ?? DEFAULT_FUSION_PROMPT_SETTINGS.optimizedContext
89528
+ }) ? [`<safety_rules>${FUSION_SAFETY_KERNEL}</safety_rules>`] : []],
89082
89529
  ...compressRuleset === void 0 ? [] : [`<response_style>${compressRuleset}</response_style>`],
89083
- ...fusionRole === void 0 ? [] : [`<fusion_role>${fusionRoleInstructionsWith(fusionRole, "codex", fusionPromptSettings)}</fusion_role>`],
89084
89530
  `<runtime_info>In case you're asked: you are running in P4Code through the Codex harness, as ${toSingleLine(runtime.model)} with ${toSingleLine(runtime.reasoningEffort)} reasoning effort. No need to mention this otherwise.</runtime_info>`
89085
89531
  ].join("\n\n");
89086
89532
  }
@@ -90940,7 +91386,11 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (codexConfig, o
90940
91386
  if (existing && !existing.stopped) yield* Effect.suspend(() => stopSessionInternal(existing));
90941
91387
  const serviceTier = input.modelSelection?.instanceId === boundInstanceId ? getCodexServiceTierOptionValue(input.modelSelection) : void 0;
90942
91388
  const mcpSession = readMcpProviderSession(input.threadId);
90943
- const externalMcp = toCodexMcpConfig(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, P4CODE_MCP_SERVER_NAMES);
91389
+ const resolvedFusionPromptSettings = options?.resolveFusionPromptSettings === void 0 ? void 0 : yield* options.resolveFusionPromptSettings;
91390
+ const externalMcp = toCodexMcpConfig(fusionExternalMcpServers(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, {
91391
+ role: input.fusionRole,
91392
+ optimizedContext: resolvedFusionPromptSettings?.optimizedContext ?? DEFAULT_FUSION_PROMPT_SETTINGS.optimizedContext
91393
+ }), P4CODE_MCP_SERVER_NAMES);
90944
91394
  for (const dropped of externalMcp.skipped) yield* Effect.logWarning("codex cannot express an mcp registration", {
90945
91395
  server: dropped.name,
90946
91396
  reason: dropped.reason
@@ -90949,7 +91399,6 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (codexConfig, o
90949
91399
  enableVerificationBeforeCompletion: DEFAULT_SERVER_SETTINGS.enableVerificationBeforeCompletion,
90950
91400
  enableRootCauseBeforeFix: DEFAULT_SERVER_SETTINGS.enableRootCauseBeforeFix
90951
91401
  } : yield* options.resolveGuardrailPrompts;
90952
- const resolvedFusionPromptSettings = options?.resolveFusionPromptSettings === void 0 ? void 0 : yield* options.resolveFusionPromptSettings;
90953
91402
  const guardrailPromptsEnabled = resolvedGuardrailPrompts.enableVerificationBeforeCompletion || resolvedGuardrailPrompts.enableRootCauseBeforeFix;
90954
91403
  const runtimeInput = {
90955
91404
  ...options?.onUsageLimits ? { onUsageLimits: options.onUsageLimits } : {},
@@ -91509,7 +91958,8 @@ const CodexDriver = {
91509
91958
  const resolveFusionPromptSettings = serverSettings.getSettings.pipe(Effect.map((settings) => ({
91510
91959
  promptOverrides: settings.fusionPromptOverrides,
91511
91960
  summarizerModel: resolveFusionSummarizerSelection(settings),
91512
- evidenceSummary: settings.fusionEvidenceSummary
91961
+ evidenceSummary: settings.fusionEvidenceSummary,
91962
+ optimizedContext: settings.enableOptimizedFusionPromptDelivery
91513
91963
  })), Effect.orElseSucceed(() => DEFAULT_FUSION_PROMPT_SETTINGS));
91514
91964
  const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv);
91515
91965
  const checkProvider = checkCodexProviderStatus(effectiveConfig, void 0, processEnv).pipe(Effect.map(stampIdentity), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(FileSystem.FileSystem, fileSystem));
@@ -92700,7 +93150,7 @@ function makeCursorAdapter(cursorSettings, options) {
92700
93150
  });
92701
93151
  const effectiveCursorSettings = options?.resolveSettings ? yield* options.resolveSettings : cursorSettings;
92702
93152
  const mcpSession = readMcpProviderSession(input.threadId);
92703
- const mcpServers = [...toAcpMcpServers(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, providerMcpReservedNames()), ...providerMcpEndpoints(mcpSession).map((entry) => ({
93153
+ const mcpServers = [...toAcpMcpServers(fusionExternalMcpServers(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, fusionContextPolicyFor(input.fusionRole, options?.resolveFusionContextOptimization === void 0 ? DEFAULT_FUSION_CONTEXT_OPTIMIZATION : yield* options.resolveFusionContextOptimization)), providerMcpReservedNames()), ...providerMcpEndpoints(mcpSession).map((entry) => ({
92704
93154
  type: "http",
92705
93155
  name: entry.name,
92706
93156
  url: entry.url,
@@ -93207,6 +93657,7 @@ const CursorDriver = {
93207
93657
  const adapter = yield* makeCursorAdapter(effectiveConfig, {
93208
93658
  environment: processEnv,
93209
93659
  resolveMcpServers: (yield* McpRegistry).resolveForSession,
93660
+ resolveFusionContextOptimization: serverSettings.getSettings.pipe(Effect.map((settings) => settings.enableOptimizedFusionPromptDelivery), Effect.orElseSucceed(() => DEFAULT_FUSION_CONTEXT_OPTIMIZATION)),
93210
93661
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {},
93211
93662
  instanceId
93212
93663
  });
@@ -93962,7 +94413,7 @@ function makeGrokAdapter(grokSettings, options) {
93962
94413
  threadId: input.threadId
93963
94414
  });
93964
94415
  const mcpSession = readMcpProviderSession(input.threadId);
93965
- const mcpServers = [...toAcpMcpServers(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, providerMcpReservedNames()), ...providerMcpEndpoints(mcpSession).map((entry) => ({
94416
+ const mcpServers = [...toAcpMcpServers(fusionExternalMcpServers(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, fusionContextPolicyFor(input.fusionRole, options?.resolveFusionContextOptimization === void 0 ? DEFAULT_FUSION_CONTEXT_OPTIMIZATION : yield* options.resolveFusionContextOptimization)), providerMcpReservedNames()), ...providerMcpEndpoints(mcpSession).map((entry) => ({
93966
94417
  type: "http",
93967
94418
  name: entry.name,
93968
94419
  url: entry.url,
@@ -94806,6 +95257,7 @@ const GrokDriver = {
94806
95257
  const adapter = yield* makeGrokAdapter(effectiveConfig, {
94807
95258
  environment: processEnv,
94808
95259
  resolveMcpServers: (yield* McpRegistry).resolveForSession,
95260
+ resolveFusionContextOptimization: serverSettings.getSettings.pipe(Effect.map((settings) => settings.enableOptimizedFusionPromptDelivery), Effect.orElseSucceed(() => DEFAULT_FUSION_CONTEXT_OPTIMIZATION)),
94809
95261
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {},
94810
95262
  instanceId
94811
95263
  });
@@ -95995,7 +96447,7 @@ function makeMuseAdapter(museSettings, options) {
95995
96447
  authorizationHeader: mcpSession.authorizationHeader
95996
96448
  },
95997
96449
  additionalOwnedServers: providerMcpEndpoints(mcpSession).filter((entry) => entry.name !== P4CODE_MCP_SERVER_NAME),
95998
- externalServers: options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers
96450
+ externalServers: fusionExternalMcpServers(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, fusionContextPolicyFor(input.fusionRole, options?.resolveFusionContextOptimization === void 0 ? DEFAULT_FUSION_CONTEXT_OPTIMIZATION : yield* options.resolveFusionContextOptimization))
95999
96451
  }).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path$1.Path, path), Effect.orElseSucceed(() => void 0)) : void 0;
96000
96452
  if (overlay && overlay.skippedServers.length > 0) yield* Effect.logWarning("Muse cannot express these MCP servers", {
96001
96453
  threadId: input.threadId,
@@ -96505,7 +96957,8 @@ const MuseDriver = {
96505
96957
  const adapter = yield* makeMuseAdapter(effectiveConfig, {
96506
96958
  environment: processEnv,
96507
96959
  instanceId,
96508
- resolveMcpServers: (yield* McpRegistry).resolveForSession
96960
+ resolveMcpServers: (yield* McpRegistry).resolveForSession,
96961
+ resolveFusionContextOptimization: serverSettings.getSettings.pipe(Effect.map((settings) => settings.enableOptimizedFusionPromptDelivery), Effect.orElseSucceed(() => DEFAULT_FUSION_CONTEXT_OPTIMIZATION))
96509
96962
  });
96510
96963
  const textGeneration = yield* makeMuseTextGeneration(effectiveConfig, processEnv);
96511
96964
  const checkProvider = serverSettings.getSettings.pipe(Effect.map((settings) => settings.disabledSkills), Effect.orElseSucceed(() => [])).pipe(Effect.flatMap((disabledSkills) => checkMuseProviderStatus(effectiveConfig, processEnv).pipe(Effect.flatMap((draft) => withProviderSkills(draft, {
@@ -98079,7 +98532,7 @@ function makeOpenCodeAdapter(openCodeSettings, options) {
98079
98532
  });
98080
98533
  const mcpSession = readMcpProviderSession(input.threadId);
98081
98534
  if (!server.external) {
98082
- const externalMcpServers = toOpenCodeMcpServers(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, providerMcpReservedNames());
98535
+ const externalMcpServers = toOpenCodeMcpServers(fusionExternalMcpServers(options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers, fusionContextPolicyFor(input.fusionRole, options?.resolveFusionContextOptimization === void 0 ? DEFAULT_FUSION_CONTEXT_OPTIMIZATION : yield* options.resolveFusionContextOptimization)), providerMcpReservedNames());
98083
98536
  for (const entry of externalMcpServers) yield* runOpenCodeSdk("mcp.add", () => client.mcp.add(entry)).pipe(Effect.catchCause((cause) => Effect.logWarning("opencode rejected an mcp registration", {
98084
98537
  server: entry.name,
98085
98538
  cause
@@ -98814,6 +99267,7 @@ const BUILT_IN_DRIVERS = [
98814
99267
  instanceId,
98815
99268
  environment: processEnv,
98816
99269
  resolveMcpServers: (yield* McpRegistry).resolveForSession,
99270
+ resolveFusionContextOptimization: serverSettings.getSettings.pipe(Effect.map((settings) => settings.enableOptimizedFusionPromptDelivery), Effect.orElseSucceed(() => DEFAULT_FUSION_CONTEXT_OPTIMIZATION)),
98817
99271
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
98818
99272
  });
98819
99273
  const textGeneration = yield* makeOpenCodeTextGeneration(effectiveConfig, processEnv);
@@ -132527,14 +132981,14 @@ const resolveSpawnModelSelection = (requested, inherited) => requested === void
132527
132981
  };
132528
132982
  const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
132529
132983
  thread_plan_update: (input) => Effect.gen(function* () {
132530
- const { threadId } = yield* requireThreadCapability();
132531
- const threads = yield* ProjectionThreadRepository;
132984
+ const invocation = yield* requireThreadCapability();
132532
132985
  const reject = (detail) => new ThreadControlRejectedError({
132533
- threadId,
132986
+ threadId: invocation.threadId,
132534
132987
  commandType: "thread.activity.append",
132535
132988
  detail
132536
132989
  });
132537
- const thread = yield* threads.getById({ threadId }).pipe(Effect.mapError((cause) => reject(cause.message)));
132990
+ const threadId = input.threadId === void 0 || input.threadId === invocation.threadId ? invocation.threadId : yield* requireWatchCapability(input.threadId).pipe(Effect.as(input.threadId), Effect.catch(() => reject("This session may not update that thread's plan")));
132991
+ const thread = yield* (yield* ProjectionThreadRepository).getById({ threadId }).pipe(Effect.mapError((cause) => reject(cause.message)));
132538
132992
  if (Option.isNone(thread)) return yield* reject("Current thread does not exist.");
132539
132993
  if (input.plan.filter((step) => step.status === "in_progress").length > 1) return yield* reject("At most one phase may be in progress.");
132540
132994
  const crypto = yield* Crypto.Crypto;
@@ -133470,19 +133924,19 @@ const estimateTokens = (text) => Math.ceil(text.length / 4);
133470
133924
  /** One line per event is plenty to answer "did anything reviewable happen". */
133471
133925
  const MAX_LINE_CHARS = 400;
133472
133926
  const oneLine = (text) => text.replace(/\s+/gu, " ").trim();
133473
- const clip = (text) => text.length <= MAX_LINE_CHARS ? text : `${text.slice(0, MAX_LINE_CHARS)}…`;
133927
+ const clip$1 = (text) => text.length <= MAX_LINE_CHARS ? text : `${text.slice(0, MAX_LINE_CHARS)}…`;
133474
133928
  /** The line an event contributes, or null when it says nothing about the work. */
133475
133929
  function describeEvent(event, implementerThreadId) {
133476
133930
  switch (event.type) {
133477
133931
  case "thread.message-sent": {
133478
133932
  if (event.payload.threadId !== implementerThreadId) return null;
133479
133933
  const text = oneLine(event.payload.text);
133480
- return text.length === 0 ? null : clip(`${event.payload.role}: ${text}`);
133934
+ return text.length === 0 ? null : clip$1(`${event.payload.role}: ${text}`);
133481
133935
  }
133482
133936
  case "thread.activity-appended": {
133483
133937
  if (event.payload.threadId !== implementerThreadId) return null;
133484
133938
  const activity = event.payload.activity;
133485
- return clip(oneLine(`${activity.kind}: ${activity.summary}`));
133939
+ return clip$1(oneLine(`${activity.kind}: ${activity.summary}`));
133486
133940
  }
133487
133941
  case "thread.turn-completed":
133488
133942
  if (event.payload.threadId !== implementerThreadId) return null;
@@ -133603,10 +134057,32 @@ const ThreadWatchReviewTool = Tool.make("thread_watch_review", {
133603
134057
  McpInvocationContext,
133604
134058
  ThreadEventStreamService,
133605
134059
  OrchestrationEngineService,
134060
+ ProjectionSnapshotQuery,
134061
+ Crypto.Crypto
134062
+ ]
134063
+ }).annotate(Tool.Title, "Read compact thread review").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
134064
+ /**
134065
+ * What the supervisor says happened to the evidence it just read.
134066
+ *
134067
+ * Without it the review path is unobservable: a summarized read, a raw
134068
+ * fallback and a supervisor that never spawned anything all look identical
134069
+ * from outside. The context numbers are not taken from the report - the server
134070
+ * reads them off the reporting thread's own context-window rows - so the one
134071
+ * number a supervisor could be wrong about is the one it does not supply.
134072
+ */
134073
+ const ThreadEvidenceReportTool = Tool.make("thread_evidence_report", {
134074
+ description: "Report what happened to the builder evidence you just read: outcome summarized with the summarizer's exact text, fallback when you read the raw range yourself, or failed when the summarizer could not run or report. Give the sequence range you covered and the model you spawned on. Call it once per review, after the last page. The server records it and correlates your own context usage; do not include builder tool output.",
134075
+ parameters: ThreadEvidenceReportInput,
134076
+ success: ThreadEvidenceReportResult,
134077
+ failure: ThreadWatchToolError,
134078
+ dependencies: [
134079
+ McpInvocationContext,
134080
+ OrchestrationEngineService,
134081
+ ProjectionSnapshotQuery,
133606
134082
  Crypto.Crypto
133607
134083
  ]
133608
- }).annotate(Tool.Title, "Read compact thread review").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
133609
- const WatchToolkit = Toolkit.make(ThreadWatchEventsTool, ThreadWatchReviewTool);
134084
+ }).annotate(Tool.Title, "Report evidence summary").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
134085
+ const WatchToolkit = Toolkit.make(ThreadWatchEventsTool, ThreadWatchReviewTool, ThreadEvidenceReportTool);
133610
134086
  //#endregion
133611
134087
  //#region src/mcp/toolkits/watch/handlers.ts
133612
134088
  /**
@@ -133618,6 +134094,15 @@ const WatchToolkit = Toolkit.make(ThreadWatchEventsTool, ThreadWatchReviewTool);
133618
134094
  * appending beside it.
133619
134095
  */
133620
134096
  const evidenceActivityId = (watchedThreadId, throughSequence) => EventId.make(`fusion-evidence:${watchedThreadId}:${throughSequence}`);
134097
+ /** One report per review, keyed like the read it reports on so a retry replaces. */
134098
+ const reportActivityId = (watchedThreadId, lastSequence) => EventId.make(`fusion-evidence-summary:${watchedThreadId}:${lastSequence}`);
134099
+ const MODEL_ID_MAX_CHARS = 120;
134100
+ /** Plain words, because the client capitalizes and renders them as the row heading. */
134101
+ const REPORT_HEADING = {
134102
+ summarized: "Evidence summary",
134103
+ fallback: "Evidence read raw",
134104
+ failed: "Evidence summary failed"
134105
+ };
133621
134106
  /**
133622
134107
  * Records that the supervisor read the builder's evidence, in its own thread.
133623
134108
  *
@@ -133628,7 +134113,10 @@ const evidenceActivityId = (watchedThreadId, throughSequence) => EventId.make(`f
133628
134113
  const recordEvidenceRead = Effect.fn("mcp.watch.recordEvidenceRead")(function* (readerThreadId, page) {
133629
134114
  const engine = yield* OrchestrationEngineService;
133630
134115
  const crypto = yield* Crypto.Crypto;
134116
+ const projections = yield* ProjectionSnapshotQuery;
133631
134117
  const createdAt = DateTime.formatIso(yield* DateTime.now);
134118
+ const recorded = yield* projections.getEvidenceReadContext(evidenceActivityId(page.threadId, page.throughSequence)).pipe(Effect.orElseSucceed(() => Option.none()));
134119
+ const usedTokensAtRead = Option.isSome(recorded) ? recorded.value : yield* projections.getLatestThreadContextWindow(readerThreadId).pipe(Effect.map(Option.map((row) => row.usedTokens)), Effect.orElseSucceed(() => Option.none()), Effect.map(Option.getOrNull));
133632
134120
  yield* engine.dispatch({
133633
134121
  type: "thread.activity.append",
133634
134122
  commandId: CommandId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)),
@@ -133646,7 +134134,8 @@ const recordEvidenceRead = Effect.fn("mcp.watch.recordEvidenceRead")(function* (
133646
134134
  throughSequence: page.throughSequence,
133647
134135
  nextAfterSequence: page.nextAfterSequence,
133648
134136
  hasMore: page.hasMore,
133649
- scannedEvents: page.scannedEvents
134137
+ scannedEvents: page.scannedEvents,
134138
+ ...usedTokensAtRead === null ? {} : { usedTokensAtRead }
133650
134139
  }
133651
134140
  }
133652
134141
  }).pipe(Effect.catchCause((cause) => Effect.logWarning("could not record the supervisor's evidence read", {
@@ -133655,6 +134144,36 @@ const recordEvidenceRead = Effect.fn("mcp.watch.recordEvidenceRead")(function* (
133655
134144
  cause
133656
134145
  })));
133657
134146
  });
134147
+ /** Bounded so one report cannot dominate a thread's activity storage. */
134148
+ const clip = (value, limit) => {
134149
+ const trimmed = value?.trim();
134150
+ if (!trimmed) return null;
134151
+ return trimmed.length <= limit ? trimmed : `${trimmed.slice(0, limit)}…`;
134152
+ };
134153
+ /**
134154
+ * The reporting thread's own context around the read it is reporting on.
134155
+ *
134156
+ * `before` comes off the evidence-read activity rather than the report, so the
134157
+ * pair of numbers brackets the ingestion instead of describing only the moment
134158
+ * the supervisor got around to reporting. A thread with no context-window row
134159
+ * yet yields null rather than a zero that would read as a real measurement.
134160
+ */
134161
+ const correlateContext = Effect.fn("mcp.watch.correlateContext")(function* (readerThreadId, readActivityId) {
134162
+ const projections = yield* ProjectionSnapshotQuery;
134163
+ const latest = yield* projections.getLatestThreadContextWindow(readerThreadId).pipe(Effect.orElseSucceed(() => Option.none()));
134164
+ const before = yield* projections.getEvidenceReadContext(readActivityId).pipe(Effect.orElseSucceed(() => Option.none()));
134165
+ if (Option.isNone(latest)) return null;
134166
+ const usedTokensAfter = latest.value.usedTokens;
134167
+ const usedTokensBefore = Option.getOrNull(before);
134168
+ const maxTokens = latest.value.maxTokens;
134169
+ return {
134170
+ usedTokensBefore,
134171
+ usedTokensAfter,
134172
+ deltaTokens: usedTokensBefore === null ? null : usedTokensAfter - usedTokensBefore,
134173
+ maxTokens,
134174
+ capacityPercent: maxTokens === null || maxTokens <= 0 ? null : Math.round(usedTokensAfter / maxTokens * 1e3) / 10
134175
+ };
134176
+ });
133658
134177
  const WatchToolkitHandlersLive = WatchToolkit.toLayer({
133659
134178
  thread_watch_review: (input) => Effect.gen(function* () {
133660
134179
  const invocation = yield* requireWatchCapability(input.threadId);
@@ -133665,6 +134184,48 @@ const WatchToolkitHandlersLive = WatchToolkit.toLayer({
133665
134184
  yield* recordEvidenceRead(invocation.threadId, page);
133666
134185
  return page;
133667
134186
  }),
134187
+ thread_evidence_report: (input) => Effect.gen(function* () {
134188
+ const invocation = yield* requireWatchCapability(input.threadId);
134189
+ const engine = yield* OrchestrationEngineService;
134190
+ const crypto = yield* Crypto.Crypto;
134191
+ const createdAt = DateTime.formatIso(yield* DateTime.now);
134192
+ const context = yield* correlateContext(invocation.threadId, evidenceActivityId(input.threadId, input.throughSequence));
134193
+ const summary = clip(input.summary, THREAD_EVIDENCE_REPORT_MAX_CHARS);
134194
+ const reason = clip(input.reason, 500);
134195
+ const outcome = summary === null && input.outcome === "summarized" ? "failed" : input.outcome;
134196
+ return {
134197
+ recorded: yield* engine.dispatch({
134198
+ type: "thread.activity.append",
134199
+ commandId: CommandId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)),
134200
+ threadId: invocation.threadId,
134201
+ createdAt,
134202
+ activity: {
134203
+ id: reportActivityId(input.threadId, input.throughSequence),
134204
+ createdAt,
134205
+ tone: outcome === "failed" ? "error" : "info",
134206
+ kind: "fusion.evidence.summary",
134207
+ summary: REPORT_HEADING[outcome],
134208
+ turnId: null,
134209
+ payload: {
134210
+ watchedThreadId: input.threadId,
134211
+ firstSequence: input.firstSequence,
134212
+ lastSequence: input.lastSequence,
134213
+ throughSequence: input.throughSequence,
134214
+ outcome,
134215
+ summary,
134216
+ model: clip(input.model, MODEL_ID_MAX_CHARS),
134217
+ reason: reason ?? (outcome === "summarized" ? null : "the supervisor gave no reason"),
134218
+ context
134219
+ }
134220
+ }
134221
+ }).pipe(Effect.as(true), Effect.catchCause((cause) => Effect.logWarning("could not record the supervisor's evidence report", {
134222
+ readerThreadId: invocation.threadId,
134223
+ watchedThreadId: input.threadId,
134224
+ cause
134225
+ }).pipe(Effect.as(false)))),
134226
+ context
134227
+ };
134228
+ }),
133668
134229
  thread_watch_events: (input) => Effect.gen(function* () {
133669
134230
  yield* requireWatchCapability(input.threadId);
133670
134231
  const page = yield* (yield* ThreadEventStreamService).read({
@@ -137405,7 +137966,8 @@ const make$6 = Effect.gen(function* () {
137405
137966
  const fusionPromptSettings = yield* serverSettingsService.getSettings.pipe(Effect.map((value) => ({
137406
137967
  promptOverrides: value.fusionPromptOverrides,
137407
137968
  summarizerModel: resolveFusionSummarizerSelection(value),
137408
- evidenceSummary: value.fusionEvidenceSummary
137969
+ evidenceSummary: value.fusionEvidenceSummary,
137970
+ optimizedContext: value.enableOptimizedFusionPromptDelivery
137409
137971
  })), Effect.catchCause(() => Effect.succeed(DEFAULT_FUSION_PROMPT_SETTINGS)));
137410
137972
  const activeFusionPair = findActiveFusionPair(commandReadModel.threadPairs, input.threadId);
137411
137973
  const fusionRole = activeFusionPair === void 0 ? void 0 : activeFusionPair.implementerThreadId === input.threadId ? "implementer" : "watcher";