@p4code/cli 0.5.15 → 0.5.17
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 +592 -83
- package/dist/client/assets/DiffPanel-BX6oPj-G.js +98 -0
- package/dist/client/assets/{FilePreviewPanel-oKmAHtSz.js → FilePreviewPanel-Bh2tvvsr.js} +6 -6
- package/dist/client/assets/PreviewPanel-Ds2Q7clx.js +2 -0
- package/dist/client/assets/PullRequestCodeTab-BqU0ufjO.js +251 -0
- package/dist/client/assets/{fileCommentAnnotations-CoOCi5GC.js → fileCommentAnnotations-DFTgO2Eq.js} +2 -2
- package/dist/client/assets/index-DUp1fSQm.js +2929 -0
- package/dist/client/assets/{previewAssetResource-CKOao7Zb.js → previewAssetResource-CiAACum3.js} +3 -3
- package/dist/client/assets/{renderFileChildren-CC342tNt.js → renderFileChildren-LNOBfS1K.js} +2 -2
- package/dist/client/assets/{toggle-group-hqQeP7ri.js → toggle-group-DFyg0Pe6.js} +2 -2
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/dist/client/assets/DiffPanel-BnLuM3jo.js +0 -98
- package/dist/client/assets/PreviewPanel-DjTevyic.js +0 -2
- package/dist/client/assets/PullRequestCodeTab-L4j_GNQB.js +0 -251
- package/dist/client/assets/index-CL8KBlld.js +0 -2918
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.
|
|
268
|
+
var version$1 = "0.5.17";
|
|
269
269
|
//#endregion
|
|
270
270
|
//#region src/config.ts
|
|
271
271
|
/**
|
|
@@ -12960,8 +12960,10 @@ const ThreadEvidenceActivityOutcome = Schema$1.Literals([
|
|
|
12960
12960
|
* Server clock around one review, never the supervisor's own account of it.
|
|
12961
12961
|
*
|
|
12962
12962
|
* `readAt` is when the evidence-read row was recorded and `closedAt` when the
|
|
12963
|
-
* row that closes the review was: the report for a reported review, the
|
|
12964
|
-
*
|
|
12963
|
+
* row that closes the review was: the report for a reported review, the
|
|
12964
|
+
* completed supervisor turn for an unreported one. A supervisor turn that
|
|
12965
|
+
* failed or was interrupted closes nothing, so its read keeps no timing until
|
|
12966
|
+
* some later turn reports on it.
|
|
12965
12967
|
*/
|
|
12966
12968
|
const ThreadEvidenceReportTiming = Schema$1.Struct({
|
|
12967
12969
|
readAt: IsoDateTime,
|
|
@@ -29729,13 +29731,17 @@ function parseMarkdownFrontmatter(contents) {
|
|
|
29729
29731
|
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
29730
29732
|
const description = typeof record.description === "string" ? record.description.trim() : "";
|
|
29731
29733
|
const disableModelInvocation = readFrontmatterBoolean(record["disable-model-invocation"]);
|
|
29734
|
+
const tools = Array.isArray(record.tools) ? record.tools.flatMap((tool) => typeof tool === "string" && tool.trim() ? [tool.trim()] : []) : void 0;
|
|
29735
|
+
const model = typeof record.model === "string" ? record.model.trim() : "";
|
|
29732
29736
|
const requiredInteractionMode = record["required-interaction-mode"] === "default" || record["required-interaction-mode"] === "plan" ? record["required-interaction-mode"] : void 0;
|
|
29733
29737
|
return {
|
|
29734
29738
|
kind: "parsed",
|
|
29735
29739
|
...name ? { name } : {},
|
|
29736
29740
|
...description ? { description } : {},
|
|
29737
29741
|
...disableModelInvocation === void 0 ? {} : { disableModelInvocation },
|
|
29738
|
-
...requiredInteractionMode === void 0 ? {} : { requiredInteractionMode }
|
|
29742
|
+
...requiredInteractionMode === void 0 ? {} : { requiredInteractionMode },
|
|
29743
|
+
...tools && tools.length > 0 ? { tools } : {},
|
|
29744
|
+
...model ? { model } : {}
|
|
29739
29745
|
};
|
|
29740
29746
|
}
|
|
29741
29747
|
//#endregion
|
|
@@ -30039,6 +30045,42 @@ const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* (config
|
|
|
30039
30045
|
scope: "project"
|
|
30040
30046
|
}] : []], disabledSkills);
|
|
30041
30047
|
});
|
|
30048
|
+
/**
|
|
30049
|
+
* Load named subagent definitions from the user config dir.
|
|
30050
|
+
*
|
|
30051
|
+
* The CLI finds these itself whenever the `user` setting source is loaded, so
|
|
30052
|
+
* nothing calls this on an ordinary session. It exists for the sessions that
|
|
30053
|
+
* drop that source and still owe the user a specific agent - a Fusion half
|
|
30054
|
+
* under the lean context profile, which must still run the delivery reviewer.
|
|
30055
|
+
* Reading only the names asked for keeps that session from paying for the rest
|
|
30056
|
+
* of the user's agents, which is what dropping the source bought.
|
|
30057
|
+
*
|
|
30058
|
+
* Best-effort like the skill discovery beside it: a missing or malformed
|
|
30059
|
+
* definition is skipped rather than failing the session, because a session
|
|
30060
|
+
* that cannot start is worse than one missing an agent.
|
|
30061
|
+
*/
|
|
30062
|
+
const discoverClaudeUserAgents = Effect.fn("discoverClaudeUserAgents")(function* (config, names, environment, cwd) {
|
|
30063
|
+
if (names.length === 0) return {};
|
|
30064
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
30065
|
+
const path = yield* Path$1.Path;
|
|
30066
|
+
const agentsDir = yield* resolveClaudeUserAgentsDir(config, environment, cwd);
|
|
30067
|
+
const definitions = {};
|
|
30068
|
+
for (const name of names) {
|
|
30069
|
+
const contents = yield* fileSystem.readFileString(path.join(agentsDir, `${name}.md`)).pipe(Effect.orElseSucceed(() => void 0));
|
|
30070
|
+
if (contents === void 0) continue;
|
|
30071
|
+
const frontmatter = parseMarkdownFrontmatter(contents);
|
|
30072
|
+
if (frontmatter.kind !== "parsed") continue;
|
|
30073
|
+
const prompt = stripMarkdownFrontmatter(contents);
|
|
30074
|
+
if (frontmatter.description === void 0 || prompt.length === 0) continue;
|
|
30075
|
+
definitions[name] = {
|
|
30076
|
+
description: frontmatter.description,
|
|
30077
|
+
prompt,
|
|
30078
|
+
...frontmatter.tools ? { tools: frontmatter.tools } : {},
|
|
30079
|
+
...frontmatter.model ? { model: frontmatter.model } : {}
|
|
30080
|
+
};
|
|
30081
|
+
}
|
|
30082
|
+
return definitions;
|
|
30083
|
+
});
|
|
30042
30084
|
//#endregion
|
|
30043
30085
|
//#region src/mcp/ClaudeMcpFiles.ts
|
|
30044
30086
|
/**
|
|
@@ -41618,7 +41660,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
|
|
|
41618
41660
|
const EvidenceReadContextRow = Schema$1.Struct({
|
|
41619
41661
|
usedTokensAtRead: Schema$1.NullOr(Schema$1.Number),
|
|
41620
41662
|
readAt: Schema$1.String,
|
|
41621
|
-
afterSequence: Schema$1.NullOr(Schema$1.Number)
|
|
41663
|
+
afterSequence: Schema$1.NullOr(Schema$1.Number),
|
|
41664
|
+
throughSequence: Schema$1.NullOr(Schema$1.Number)
|
|
41622
41665
|
});
|
|
41623
41666
|
const getEvidenceReadContextRow = SqlSchema.findOneOption({
|
|
41624
41667
|
Request: Schema$1.String,
|
|
@@ -41631,16 +41674,87 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
|
|
|
41631
41674
|
ELSE NULL
|
|
41632
41675
|
END AS "usedTokensAtRead",
|
|
41633
41676
|
created_at AS "readAt",
|
|
41634
|
-
json_extract(payload_json, '$.afterSequence') AS "afterSequence"
|
|
41677
|
+
json_extract(payload_json, '$.afterSequence') AS "afterSequence",
|
|
41678
|
+
json_extract(payload_json, '$.throughSequence') AS "throughSequence"
|
|
41635
41679
|
FROM projection_thread_activities
|
|
41636
41680
|
WHERE activity_id = ${activityId}
|
|
41637
41681
|
LIMIT 1
|
|
41682
|
+
`
|
|
41683
|
+
});
|
|
41684
|
+
const TurnStartRow = Schema$1.Struct({
|
|
41685
|
+
threadId: ThreadId,
|
|
41686
|
+
messageId: Schema$1.NullOr(Schema$1.String),
|
|
41687
|
+
startedAt: Schema$1.NullOr(Schema$1.String)
|
|
41688
|
+
});
|
|
41689
|
+
/**
|
|
41690
|
+
* The message a turn was started from, by the table's own key.
|
|
41691
|
+
*
|
|
41692
|
+
* Thread and turn together, because the unique key is the pair: a turn id is
|
|
41693
|
+
* provider-supplied for some providers, and matching on it alone would let
|
|
41694
|
+
* one provider's collision answer for another thread.
|
|
41695
|
+
*/
|
|
41696
|
+
const getTurnStartRow = SqlSchema.findOneOption({
|
|
41697
|
+
Request: Schema$1.Struct({
|
|
41698
|
+
threadId: ThreadId,
|
|
41699
|
+
turnId: TurnId
|
|
41700
|
+
}),
|
|
41701
|
+
Result: TurnStartRow,
|
|
41702
|
+
execute: (input) => sql`
|
|
41703
|
+
SELECT
|
|
41704
|
+
thread_id AS "threadId",
|
|
41705
|
+
pending_message_id AS "messageId",
|
|
41706
|
+
started_at AS "startedAt"
|
|
41707
|
+
FROM projection_turns
|
|
41708
|
+
WHERE thread_id = ${input.threadId}
|
|
41709
|
+
AND turn_id = ${input.turnId}
|
|
41710
|
+
LIMIT 1
|
|
41711
|
+
`
|
|
41712
|
+
});
|
|
41713
|
+
/** The turn a thread's session is running, by the session projection's own row. */
|
|
41714
|
+
const getActiveTurnIdRow = SqlSchema.findOneOption({
|
|
41715
|
+
Request: ThreadId,
|
|
41716
|
+
Result: Schema$1.Struct({ activeTurnId: Schema$1.NullOr(Schema$1.String) }),
|
|
41717
|
+
execute: (threadId) => sql`
|
|
41718
|
+
SELECT active_turn_id AS "activeTurnId"
|
|
41719
|
+
FROM projection_thread_sessions
|
|
41720
|
+
WHERE thread_id = ${threadId}
|
|
41721
|
+
LIMIT 1
|
|
41722
|
+
`
|
|
41723
|
+
});
|
|
41724
|
+
const ThreadMessageStampRow = Schema$1.Struct({
|
|
41725
|
+
messageId: Schema$1.String,
|
|
41726
|
+
createdAt: Schema$1.String
|
|
41727
|
+
});
|
|
41728
|
+
/**
|
|
41729
|
+
* The newest message on a thread whose id starts with a prefix.
|
|
41730
|
+
*
|
|
41731
|
+
* The server names the messages it sends itself, so asking by id prefix asks
|
|
41732
|
+
* what the server last sent - a question no turn row can be talked out of by
|
|
41733
|
+
* whichever turn happened to adopt a pending start.
|
|
41734
|
+
*/
|
|
41735
|
+
const getLatestThreadMessageWithIdPrefixRow = SqlSchema.findOneOption({
|
|
41736
|
+
Request: Schema$1.Struct({
|
|
41737
|
+
threadId: ThreadId,
|
|
41738
|
+
idPrefix: Schema$1.String
|
|
41739
|
+
}),
|
|
41740
|
+
Result: ThreadMessageStampRow,
|
|
41741
|
+
execute: (input) => sql`
|
|
41742
|
+
SELECT
|
|
41743
|
+
message_id AS "messageId",
|
|
41744
|
+
created_at AS "createdAt"
|
|
41745
|
+
FROM projection_thread_messages
|
|
41746
|
+
WHERE thread_id = ${input.threadId}
|
|
41747
|
+
AND message_id LIKE ${`${input.idPrefix}%`} ESCAPE '\\'
|
|
41748
|
+
ORDER BY created_at DESC, message_id DESC
|
|
41749
|
+
LIMIT 1
|
|
41638
41750
|
`
|
|
41639
41751
|
});
|
|
41640
41752
|
const LatestEvidenceReadRow = Schema$1.Struct({
|
|
41641
41753
|
watchedThreadId: ThreadId,
|
|
41642
41754
|
afterSequence: Schema$1.NullOr(Schema$1.Number),
|
|
41643
41755
|
throughSequence: Schema$1.Number,
|
|
41756
|
+
/** The supervisor turn that recorded the read, absent on rows written before it was stamped. */
|
|
41757
|
+
turnId: Schema$1.NullOr(Schema$1.String),
|
|
41644
41758
|
readAt: Schema$1.String,
|
|
41645
41759
|
hasReport: Schema$1.Number
|
|
41646
41760
|
});
|
|
@@ -41659,6 +41773,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
|
|
|
41659
41773
|
json_extract(reads.payload_json, '$.watchedThreadId') AS "watchedThreadId",
|
|
41660
41774
|
json_extract(reads.payload_json, '$.afterSequence') AS "afterSequence",
|
|
41661
41775
|
json_extract(reads.payload_json, '$.throughSequence') AS "throughSequence",
|
|
41776
|
+
json_extract(reads.payload_json, '$.turnId') AS "turnId",
|
|
41662
41777
|
reads.created_at AS "readAt",
|
|
41663
41778
|
EXISTS (
|
|
41664
41779
|
SELECT 1
|
|
@@ -41674,6 +41789,10 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
|
|
|
41674
41789
|
WHERE reads.thread_id = ${threadId}
|
|
41675
41790
|
AND reads.kind = 'fusion.evidence.read'
|
|
41676
41791
|
AND json_extract(reads.payload_json, '$.throughSequence') >= 0
|
|
41792
|
+
-- A raw row's bound is wherever paging reached, never the bound a
|
|
41793
|
+
-- wake named, so no report can pair with it and an unpaired one must
|
|
41794
|
+
-- not read as a review nobody answered.
|
|
41795
|
+
AND COALESCE(json_extract(reads.payload_json, '$.raw'), 0) = 0
|
|
41677
41796
|
ORDER BY reads.created_at DESC, reads.rowid DESC
|
|
41678
41797
|
LIMIT 1
|
|
41679
41798
|
`
|
|
@@ -43467,10 +43586,20 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
|
|
|
43467
43586
|
});
|
|
43468
43587
|
const getLatestThreadContextWindow = (threadId) => getLatestContextWindowRow(threadId).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getLatestThreadContextWindow:query", "ProjectionSnapshotQuery.getLatestThreadContextWindow:decodeRow")));
|
|
43469
43588
|
const getEvidenceReadContext = (activityId) => getEvidenceReadContextRow(activityId).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getEvidenceReadContext:query", "ProjectionSnapshotQuery.getEvidenceReadContext:decodeRow")));
|
|
43589
|
+
const getTurnStart = (threadId, turnId) => getTurnStartRow({
|
|
43590
|
+
threadId,
|
|
43591
|
+
turnId
|
|
43592
|
+
}).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getTurnStart:query", "ProjectionSnapshotQuery.getTurnStart:decodeRow")));
|
|
43593
|
+
const getActiveTurnId = (threadId) => getActiveTurnIdRow(threadId).pipe(Effect.map(Option.flatMap((row) => Option.fromNullable(row.activeTurnId))), Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getActiveTurnId:query", "ProjectionSnapshotQuery.getActiveTurnId:decodeRow")));
|
|
43594
|
+
const getLatestThreadMessageWithIdPrefix = (threadId, idPrefix) => getLatestThreadMessageWithIdPrefixRow({
|
|
43595
|
+
threadId,
|
|
43596
|
+
idPrefix
|
|
43597
|
+
}).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getLatestThreadMessageWithIdPrefix:query", "ProjectionSnapshotQuery.getLatestThreadMessageWithIdPrefix:decodeRow")));
|
|
43470
43598
|
const getLatestEvidenceRead = (threadId) => getLatestEvidenceReadRow(threadId).pipe(Effect.map(Option.map((row) => ({
|
|
43471
43599
|
watchedThreadId: row.watchedThreadId,
|
|
43472
43600
|
afterSequence: row.afterSequence,
|
|
43473
43601
|
throughSequence: row.throughSequence,
|
|
43602
|
+
turnId: row.turnId,
|
|
43474
43603
|
readAt: row.readAt,
|
|
43475
43604
|
hasReport: row.hasReport !== 0
|
|
43476
43605
|
}))), Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getLatestEvidenceRead:query", "ProjectionSnapshotQuery.getLatestEvidenceRead:decodeRow")));
|
|
@@ -43698,6 +43827,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
|
|
|
43698
43827
|
getThreadPairById,
|
|
43699
43828
|
getActiveThreadPairs,
|
|
43700
43829
|
getLatestEvidenceRead,
|
|
43830
|
+
getTurnStart,
|
|
43831
|
+
getLatestThreadMessageWithIdPrefix,
|
|
43832
|
+
getActiveTurnId,
|
|
43701
43833
|
getThreadShellById,
|
|
43702
43834
|
getThreadDetailById,
|
|
43703
43835
|
getHandoffContext,
|
|
@@ -59363,8 +59495,17 @@ const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(functio
|
|
|
59363
59495
|
* written as if the setting applied only to user-facing answers.
|
|
59364
59496
|
*/
|
|
59365
59497
|
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.`;
|
|
59366
|
-
|
|
59367
|
-
|
|
59498
|
+
/**
|
|
59499
|
+
* What a role does when a tool it wants is not its own.
|
|
59500
|
+
*
|
|
59501
|
+
* Both halves get the same words because the failure is the same on both
|
|
59502
|
+
* sides: a denied capability is an answer about what this role may do, and
|
|
59503
|
+
* treating it as an obstacle to route around turns a boundary the server
|
|
59504
|
+
* enforces into one it merely suggests.
|
|
59505
|
+
*/
|
|
59506
|
+
const FUSION_TOOL_BOUNDARY_INSTRUCTIONS = `A tool missing from your role, or a call the server denies for want of a capability or a grant, is a limit to report rather than an obstacle to route around. Never substitute the server's own database, its HTTP API, another MCP server, or any other back channel for a capability you were not granted; say which tool or grant you lacked and what you would have done with it.`;
|
|
59507
|
+
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. The review tools are Supervisor's half of the pair, not yours: thread_watch_review, thread_watch_events and thread_evidence_report are its tools, and a thread outside this pair is not yours to read. Your provider may still list them; that is not a grant, and every call fails. The tracker is Supervisor's too: you neither read nor write tickets, so when the work needs one filed, commented, or moved, say so in your phase report and let Supervisor carry it. ${FUSION_TOOL_BOUNDARY_INSTRUCTIONS} ${FUSION_PAIR_COMMUNICATION_INSTRUCTIONS}`;
|
|
59508
|
+
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, thread_gate_respond, and the task board tools task_current, task_get, task_list, task_create and task_update. Read the board whenever planning needs it, and read a named ticket with ticket_resolve together with its comments, treating a later comment as newer intent than the description. Resolve which task that is by calling task_current with the builder's threadId, because a pair started from the board links the row to the builder and not to you; a bare task_current answers for your own thread and reports the task as unlinked. Write only within that task: update it or a subtask you created under it, never an unrelated row, never another project's board, and never any board but this one - task_create files a real ticket on an external tracker when given a source such as 'linear', and that is not yours to file. 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:
|
|
59368
59509
|
## Recommendation
|
|
59369
59510
|
State the decision or correction in one sentence. For a phase review, say whether the phase is approved or needs changes.
|
|
59370
59511
|
## Reason
|
|
@@ -59379,7 +59520,7 @@ Give ordered, concrete actions for the builder, including the next phase and its
|
|
|
59379
59520
|
* which is what it did before this existed.
|
|
59380
59521
|
*/
|
|
59381
59522
|
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.`;
|
|
59382
|
-
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}`;
|
|
59523
|
+
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_TOOL_BOUNDARY_INSTRUCTIONS} ${FUSION_PAIR_COMMUNICATION_INSTRUCTIONS}`;
|
|
59383
59524
|
/**
|
|
59384
59525
|
* The one-line stand-in for the full block on a message whose session already
|
|
59385
59526
|
* carries the role instructions.
|
|
@@ -59539,7 +59680,18 @@ const FUSION_SAFETY_KERNEL = [
|
|
|
59539
59680
|
"- Never commit, push, or open a pull request unless the current turn asked for it, and never commit to main or master directly.",
|
|
59540
59681
|
"- Never add AI attribution to a commit message, pull request, issue, comment, code, or document."
|
|
59541
59682
|
].join("\n");
|
|
59542
|
-
/**
|
|
59683
|
+
/**
|
|
59684
|
+
* Everything the supervisor half needs from the P4 MCP server, and nothing else.
|
|
59685
|
+
*
|
|
59686
|
+
* The task tools are here because planning is the supervisor's job, not an
|
|
59687
|
+
* aside to it: it owns the phase plan, so it has to be able to read the board,
|
|
59688
|
+
* break the work down and keep the authorized task honest. Mutation stays
|
|
59689
|
+
* bounded by the prompt rather than by this set, which can only say whether a
|
|
59690
|
+
* tool exists - the rule is the current task, never someone else's board row.
|
|
59691
|
+
* `task_propose` is deliberately absent: it tells its caller to stop and wait
|
|
59692
|
+
* for the user, and a supervisor that waits is one the builder sits idle
|
|
59693
|
+
* behind. Asking the user is what `ask_user_question` is for.
|
|
59694
|
+
*/
|
|
59543
59695
|
const FUSION_SUPERVISOR_P4_TOOLS = /* @__PURE__ */ new Set([
|
|
59544
59696
|
"thread_watch_review",
|
|
59545
59697
|
"thread_watch_events",
|
|
@@ -59548,7 +59700,12 @@ const FUSION_SUPERVISOR_P4_TOOLS = /* @__PURE__ */ new Set([
|
|
|
59548
59700
|
"thread_gate_respond",
|
|
59549
59701
|
"thread_plan_update",
|
|
59550
59702
|
"ask_user_question",
|
|
59551
|
-
"task_current"
|
|
59703
|
+
"task_current",
|
|
59704
|
+
"task_get",
|
|
59705
|
+
"task_list",
|
|
59706
|
+
"task_create",
|
|
59707
|
+
"task_update",
|
|
59708
|
+
"ticket_resolve"
|
|
59552
59709
|
]);
|
|
59553
59710
|
/**
|
|
59554
59711
|
* What the builder half must not reach.
|
|
@@ -59556,15 +59713,27 @@ const FUSION_SUPERVISOR_P4_TOOLS = /* @__PURE__ */ new Set([
|
|
|
59556
59713
|
* Framed as a denial rather than an allowance because the builder's job is
|
|
59557
59714
|
* open-ended: it needs coding, verification and preview tools, and a new one of
|
|
59558
59715
|
* those should reach it without an edit here. What it must never have is the
|
|
59559
|
-
* pair's own machinery - spawning threads, creating
|
|
59560
|
-
*
|
|
59561
|
-
*
|
|
59562
|
-
*
|
|
59716
|
+
* pair's own machinery - spawning threads, creating pairs, advising, answering
|
|
59717
|
+
* its own gate - the phase plan, which the supervisor owns outright, plus
|
|
59718
|
+
* task-board mutation and the external services that belong to a person rather
|
|
59719
|
+
* than to an implementation turn. The review tools join that list: they read
|
|
59720
|
+
* the review of the builder's own work, which is the supervisor watching the
|
|
59721
|
+
* builder and never the other way round.
|
|
59722
|
+
*
|
|
59723
|
+
* `thread_pair_detach` is the one piece of pair machinery the builder keeps,
|
|
59724
|
+
* because it is the way out of the arrangement rather than a way to do the
|
|
59725
|
+
* supervisor's job. `thread_pair_detach`'s own handler already limits it to a
|
|
59726
|
+
* pair whose implementer is the calling thread and whose supervisor that
|
|
59727
|
+
* thread created, so the only pair a builder can end is one it made of itself.
|
|
59728
|
+
* Denying it here would leave no agent able to detach any pair at all, which
|
|
59729
|
+
* is the one-way door this repository treats as a bug.
|
|
59563
59730
|
*/
|
|
59564
59731
|
const FUSION_BUILDER_DENIED_P4_TOOLS = /* @__PURE__ */ new Set([
|
|
59565
59732
|
"thread_spawn",
|
|
59733
|
+
"thread_watch_review",
|
|
59734
|
+
"thread_watch_events",
|
|
59735
|
+
"thread_evidence_report",
|
|
59566
59736
|
"thread_pair_create",
|
|
59567
|
-
"thread_pair_detach",
|
|
59568
59737
|
"thread_cleanup",
|
|
59569
59738
|
"thread_rename",
|
|
59570
59739
|
"thread_configure",
|
|
@@ -59609,21 +59778,90 @@ function fusionContextPolicyFor(role, optimizedContext) {
|
|
|
59609
59778
|
optimizedContext
|
|
59610
59779
|
};
|
|
59611
59780
|
}
|
|
59612
|
-
/**
|
|
59781
|
+
/**
|
|
59782
|
+
* Whether a role may call one P4 tool at all, regardless of any setting.
|
|
59783
|
+
*
|
|
59784
|
+
* The hiding functions below are a context measure: they keep a role's tool
|
|
59785
|
+
* list short, they only work on adapters that expose a per-session filter, and
|
|
59786
|
+
* the user can turn them off. This is the authorization question instead, so
|
|
59787
|
+
* it takes no setting and no adapter - a role that cannot act on a tool must
|
|
59788
|
+
* not be able to call it on any provider, whether or not the list it was shown
|
|
59789
|
+
* mentioned it.
|
|
59790
|
+
*
|
|
59791
|
+
* Shaped from the same two sets the hiding uses, so the list a role sees and
|
|
59792
|
+
* the calls it may make cannot drift apart. A thread with no role is not half
|
|
59793
|
+
* of a pair and is not this function's business.
|
|
59794
|
+
*/
|
|
59795
|
+
function fusionRoleMayCallP4Tool(role, toolName) {
|
|
59796
|
+
if (role === void 0) return true;
|
|
59797
|
+
return role === "watcher" ? FUSION_SUPERVISOR_P4_TOOLS.has(toolName) : !FUSION_BUILDER_DENIED_P4_TOOLS.has(toolName);
|
|
59798
|
+
}
|
|
59799
|
+
/**
|
|
59800
|
+
* Whether the context trimming applies. Ordinary threads never reach it.
|
|
59801
|
+
*
|
|
59802
|
+
* Only the measures that exist to save tokens ask this: the P4 tool list and
|
|
59803
|
+
* the setting sources. What a role may do is not a token question and must not
|
|
59804
|
+
* consult a setting the user can turn off - see `fusionRoleApplies`.
|
|
59805
|
+
*/
|
|
59613
59806
|
function fusionLeanContextApplies(input) {
|
|
59614
59807
|
return input.role !== void 0 && input.optimizedContext;
|
|
59615
59808
|
}
|
|
59616
59809
|
/**
|
|
59617
|
-
*
|
|
59810
|
+
* Whether a role's restrictions apply: whenever there is a role.
|
|
59811
|
+
*
|
|
59812
|
+
* Kept apart from the trimming above because the two answer different
|
|
59813
|
+
* questions. Turning context optimization off should cost a pair its token
|
|
59814
|
+
* savings, never hand the builder the tracker and the planning tools back or
|
|
59815
|
+
* give the supervisor an editor.
|
|
59816
|
+
*/
|
|
59817
|
+
function fusionRoleApplies(input) {
|
|
59818
|
+
return input.role !== void 0;
|
|
59819
|
+
}
|
|
59820
|
+
/**
|
|
59821
|
+
* The registered servers the supervisor half keeps, by lowercased name.
|
|
59822
|
+
*
|
|
59823
|
+
* Only the tracker, and only for the supervisor. It researches the request
|
|
59824
|
+
* before it plans, and `ticket_resolve` answers with a ticket's description
|
|
59825
|
+
* alone - the comments that routinely carry newer intent are reachable only
|
|
59826
|
+
* through the user's own Linear registration. The builder keeps none: it
|
|
59827
|
+
* implements the phase it was given and has no errand on a tracker.
|
|
59828
|
+
*
|
|
59829
|
+
* The name is repeated here rather than imported from `mcp/LinearMcpClient`,
|
|
59830
|
+
* which every adapter would then pull into a provider process. A test pins
|
|
59831
|
+
* the two together, exactly as the P4 tool catalogue is pinned.
|
|
59832
|
+
*/
|
|
59833
|
+
const FUSION_SUPERVISOR_EXTERNAL_MCP_SERVERS = /* @__PURE__ */ new Set(["linear"]);
|
|
59834
|
+
/**
|
|
59835
|
+
* The name a registration was given, with the user-scope prefix taken back off.
|
|
59836
|
+
*
|
|
59837
|
+
* A server the user registered in their own provider config reaches a session
|
|
59838
|
+
* re-keyed as `p4-user-<name>`, so matching the raw key would look for a name
|
|
59839
|
+
* the registry never hands over and would drop the tracker it was written to
|
|
59840
|
+
* keep. The prefix is spelled here for the same reason the name is: importing
|
|
59841
|
+
* the registry would pull it into every provider process.
|
|
59842
|
+
*/
|
|
59843
|
+
const USER_SCOPE_REGISTRATION_PREFIX = "p4-user-";
|
|
59844
|
+
const fusionRegistrationBareName = (name) => {
|
|
59845
|
+
const lowered = name.toLowerCase();
|
|
59846
|
+
return lowered.startsWith(USER_SCOPE_REGISTRATION_PREFIX) ? lowered.slice(8) : lowered;
|
|
59847
|
+
};
|
|
59848
|
+
/**
|
|
59849
|
+
* The external MCP registrations a Fusion session keeps.
|
|
59850
|
+
*
|
|
59851
|
+
* Applies to any pair turn, optimized or not: which servers a role may reach
|
|
59852
|
+
* is authorization, and the setting only governs what is trimmed to save
|
|
59853
|
+
* tokens.
|
|
59618
59854
|
*
|
|
59619
|
-
* Everything a user registered for their own work goes, because neither
|
|
59620
|
-
* can act on it during a pair turn and every definition is paid for on
|
|
59621
|
-
* turn of the loop. Computer Use needs no exception here - it is a
|
|
59855
|
+
* Everything else a user registered for their own work goes, because neither
|
|
59856
|
+
* role can act on it during a pair turn and every definition is paid for on
|
|
59857
|
+
* every turn of the loop. Computer Use needs no exception here - it is a
|
|
59622
59858
|
* provider-owned endpoint attached only to a session that was granted it, not
|
|
59623
59859
|
* a registration in this map, so a granted session keeps it either way.
|
|
59624
59860
|
*/
|
|
59625
59861
|
function fusionExternalMcpServers(servers, input) {
|
|
59626
|
-
|
|
59862
|
+
if (!fusionRoleApplies(input)) return { ...servers };
|
|
59863
|
+
if (input.role !== "watcher") return {};
|
|
59864
|
+
return Object.fromEntries(Object.entries(servers).filter(([name]) => FUSION_SUPERVISOR_EXTERNAL_MCP_SERVERS.has(fusionRegistrationBareName(name))));
|
|
59627
59865
|
}
|
|
59628
59866
|
/**
|
|
59629
59867
|
* P4 tools this role must not see, given the full catalogue.
|
|
@@ -59637,20 +59875,70 @@ function fusionHiddenP4Tools(catalogue, input) {
|
|
|
59637
59875
|
return input.role === "watcher" ? catalogue.filter((name) => !FUSION_SUPERVISOR_P4_TOOLS.has(name)) : catalogue.filter((name) => FUSION_BUILDER_DENIED_P4_TOOLS.has(name));
|
|
59638
59876
|
}
|
|
59639
59877
|
/**
|
|
59878
|
+
* Provider-native tools the supervisor must not see.
|
|
59879
|
+
*
|
|
59880
|
+
* It reviews and directs; it never edits the tree. Reading tools stay, because
|
|
59881
|
+
* grounding an objection in the code is exactly its job, and a shell stays too
|
|
59882
|
+
* - `git log` and `git status` are how it derives phase from artifacts. What
|
|
59883
|
+
* goes is the means to write the change it is supposed to be reviewing.
|
|
59884
|
+
*
|
|
59885
|
+
* Named per provider, like the builder's list, because the names are the
|
|
59886
|
+
* provider's rather than ours.
|
|
59887
|
+
*/
|
|
59888
|
+
const FUSION_SUPERVISOR_DENIED_NATIVE_TOOLS = [
|
|
59889
|
+
"Edit",
|
|
59890
|
+
"MultiEdit",
|
|
59891
|
+
"Write",
|
|
59892
|
+
"NotebookEdit",
|
|
59893
|
+
"apply_patch"
|
|
59894
|
+
];
|
|
59895
|
+
/**
|
|
59640
59896
|
* Provider-native tools this role must not see.
|
|
59641
59897
|
*
|
|
59642
59898
|
* The supervisor keeps its own planning tool: it is the half that plans, and
|
|
59643
|
-
* for its own thread that tool is the ordinary one.
|
|
59899
|
+
* for its own thread that tool is the ordinary one. It loses the editing tools
|
|
59900
|
+
* instead, which are the builder's half of the same split.
|
|
59901
|
+
*
|
|
59902
|
+
* Applies to any pair turn, optimized or not, for the same reason the external
|
|
59903
|
+
* registrations do: a builder holding the planning tool is a role problem, not
|
|
59904
|
+
* a token one.
|
|
59644
59905
|
*/
|
|
59645
59906
|
function fusionHiddenNativeTools(input) {
|
|
59646
|
-
|
|
59907
|
+
if (!fusionRoleApplies(input)) return [];
|
|
59908
|
+
return input.role === "implementer" ? FUSION_BUILDER_DENIED_NATIVE_TOOLS : FUSION_SUPERVISOR_DENIED_NATIVE_TOOLS;
|
|
59909
|
+
}
|
|
59910
|
+
/**
|
|
59911
|
+
* Global agents a pair still owes its user after the lean profile drops the
|
|
59912
|
+
* user setting source.
|
|
59913
|
+
*
|
|
59914
|
+
* Dropping that source is what makes the profile lean, but it takes the user's
|
|
59915
|
+
* subagent definitions with it, and the delivery loop this pair runs on names
|
|
59916
|
+
* one of them outright: the supervisor will not accept the work until the
|
|
59917
|
+
* global `pr-reviewer` has reviewed the branch. Leaving it unregistered means
|
|
59918
|
+
* the builder either cannot comply or quietly reviews with something else,
|
|
59919
|
+
* which is the failure this list exists to prevent. Named rather than
|
|
59920
|
+
* discovered, so the cost stays one definition instead of every agent the user
|
|
59921
|
+
* happens to have.
|
|
59922
|
+
*/
|
|
59923
|
+
const FUSION_REQUIRED_GLOBAL_AGENTS = ["pr-reviewer"];
|
|
59924
|
+
/**
|
|
59925
|
+
* Which global agents this session has to register for itself.
|
|
59926
|
+
*
|
|
59927
|
+
* Empty unless the lean profile actually dropped the user source: when it is
|
|
59928
|
+
* off the provider loads them the usual way, and registering them again would
|
|
59929
|
+
* be a second definition of the same agent.
|
|
59930
|
+
*/
|
|
59931
|
+
function fusionRequiredGlobalAgents(input) {
|
|
59932
|
+
return fusionLeanContextApplies(input) ? FUSION_REQUIRED_GLOBAL_AGENTS : [];
|
|
59647
59933
|
}
|
|
59648
59934
|
/**
|
|
59649
59935
|
* Setting sources a Fusion session reads.
|
|
59650
59936
|
*
|
|
59651
59937
|
* `user` carries the global memory files and the user-level skills; the
|
|
59652
59938
|
* project's own instructions live in `project` and `local` and stay, because
|
|
59653
|
-
* they are the ones that describe the code being changed.
|
|
59939
|
+
* they are the ones that describe the code being changed. What the drop also
|
|
59940
|
+
* takes, and what `fusionRequiredGlobalAgents` above hands back, is the user's
|
|
59941
|
+
* subagent definitions.
|
|
59654
59942
|
*/
|
|
59655
59943
|
function fusionSettingSources(sources, input) {
|
|
59656
59944
|
if (!fusionLeanContextApplies(input)) return sources;
|
|
@@ -69841,6 +70129,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
69841
70129
|
optimizedContext: fusionPromptSettings?.optimizedContext ?? DEFAULT_FUSION_PROMPT_SETTINGS.optimizedContext
|
|
69842
70130
|
};
|
|
69843
70131
|
const externalMcpServers = fusionExternalMcpServers(registeredMcpServers, fusionContextPolicy);
|
|
70132
|
+
const requiredGlobalAgents = yield* discoverClaudeUserAgents(claudeSettings, fusionRequiredGlobalAgents(fusionContextPolicy), claudeEnvironment, input.cwd).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path$1.Path, path));
|
|
69844
70133
|
const hiddenTools = [...fusionHiddenP4Tools(providerMcpToolNames(), fusionContextPolicy).map((name) => `mcp__${P4CODE_MCP_SERVER_NAME}__${name}`), ...fusionHiddenNativeTools(fusionContextPolicy)];
|
|
69845
70134
|
const unpromptedSubagents = input.unpromptedSubagents !== void 0 ? input.unpromptedSubagents : options?.resolveUnpromptedSubagents === void 0 ? DEFAULT_SERVER_SETTINGS.enableUnpromptedSubagents : yield* options.resolveUnpromptedSubagents;
|
|
69846
70135
|
const compressRuleset = compressRulesetFor(input.compressMode ?? "off");
|
|
@@ -69872,6 +70161,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
69872
70161
|
...systemPromptAppend.length > 0 ? { append: systemPromptAppend } : {}
|
|
69873
70162
|
},
|
|
69874
70163
|
settingSources: [...fusionSettingSources(CLAUDE_SETTING_SOURCES, fusionContextPolicy)],
|
|
70164
|
+
...Object.keys(requiredGlobalAgents).length > 0 ? { agents: requiredGlobalAgents } : {},
|
|
69875
70165
|
...hiddenTools.length > 0 ? { disallowedTools: hiddenTools } : {},
|
|
69876
70166
|
...effectiveEffort ? { effort: effectiveEffort } : {},
|
|
69877
70167
|
...permissionMode ? { permissionMode } : {},
|
|
@@ -130574,14 +130864,15 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130574
130864
|
const httpServer = yield* HttpServer.HttpServer;
|
|
130575
130865
|
const state = yield* SynchronizedRef.make({
|
|
130576
130866
|
records: /* @__PURE__ */ new Map(),
|
|
130577
|
-
spawnedThreads: /* @__PURE__ */ new Map()
|
|
130867
|
+
spawnedThreads: /* @__PURE__ */ new Map(),
|
|
130868
|
+
fusionRoles: /* @__PURE__ */ new Map()
|
|
130578
130869
|
});
|
|
130579
130870
|
const currentTimeMillis = options.now ? Effect.sync(options.now) : Clock.currentTimeMillis;
|
|
130580
130871
|
const livenessWindowMs = options.livenessWindowMs ?? DEFAULT_LIVENESS_WINDOW_MS;
|
|
130581
130872
|
const endpoint = httpServer.address._tag === "TcpAddress" ? `http://${getHttpMcpEndpointHost(httpServer.address.hostname)}:${httpServer.address.port}/mcp` : "http://127.0.0.1/mcp";
|
|
130582
130873
|
const hashToken = (token) => crypto.digest("SHA-256", new TextEncoder().encode(token)).pipe(Effect.map(bytesToHex), Effect.orDie);
|
|
130583
130874
|
const revokeWhere = Effect.fn("McpSessionRegistry.revokeWhere")(function* (predicate) {
|
|
130584
|
-
const removed = yield* SynchronizedRef.modify(state, ({ records, spawnedThreads }) => {
|
|
130875
|
+
const removed = yield* SynchronizedRef.modify(state, ({ records, spawnedThreads, fusionRoles }) => {
|
|
130585
130876
|
const next = new Map(records);
|
|
130586
130877
|
const scopes = [];
|
|
130587
130878
|
for (const [hash, record] of records) {
|
|
@@ -130592,7 +130883,8 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130592
130883
|
}
|
|
130593
130884
|
return [scopes, {
|
|
130594
130885
|
records: next,
|
|
130595
|
-
spawnedThreads
|
|
130886
|
+
spawnedThreads,
|
|
130887
|
+
fusionRoles
|
|
130596
130888
|
}];
|
|
130597
130889
|
});
|
|
130598
130890
|
yield* Effect.forEach(removed, (scope) => scope.authorizationLifetime?.revocation ? Deferred.succeed(scope.authorizationLifetime.revocation, void 0) : Effect.void, { discard: true });
|
|
@@ -130620,6 +130912,8 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130620
130912
|
if (watchThreadIds.size > 0) capabilities.add("watch");
|
|
130621
130913
|
if (adviseThreadIds.size > 0) capabilities.add("advise");
|
|
130622
130914
|
const threadId = ThreadId.make(request.threadId);
|
|
130915
|
+
const rememberedRole = yield* SynchronizedRef.get(state).pipe(Effect.map((current) => current.fusionRoles.get(threadId)));
|
|
130916
|
+
const fusionRole = rememberedRole === void 0 ? request.fusionRole : rememberedRole ?? void 0;
|
|
130623
130917
|
const scopeWith = (mayCreateThreads, mayCreateFusionPairs, revocation = mainRevocation) => ({
|
|
130624
130918
|
authorizationLifetime: {
|
|
130625
130919
|
revoked: false,
|
|
@@ -130633,11 +130927,12 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130633
130927
|
capabilities,
|
|
130634
130928
|
...watchThreadIds.size > 0 ? { watchThreadIds } : {},
|
|
130635
130929
|
...adviseThreadIds.size > 0 ? { adviseThreadIds } : {},
|
|
130930
|
+
...fusionRole === void 0 ? {} : { fusionRole },
|
|
130636
130931
|
mayCreateThreads,
|
|
130637
130932
|
mayCreateFusionPairs,
|
|
130638
130933
|
issuedAt
|
|
130639
130934
|
});
|
|
130640
|
-
yield* SynchronizedRef.update(state, ({ records, spawnedThreads }) => {
|
|
130935
|
+
yield* SynchronizedRef.update(state, ({ records, spawnedThreads, fusionRoles }) => {
|
|
130641
130936
|
const next = new Map(records);
|
|
130642
130937
|
next.set(tokenHash, {
|
|
130643
130938
|
tokenHash,
|
|
@@ -130657,7 +130952,8 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130657
130952
|
});
|
|
130658
130953
|
return {
|
|
130659
130954
|
records: next,
|
|
130660
|
-
spawnedThreads
|
|
130955
|
+
spawnedThreads,
|
|
130956
|
+
fusionRoles
|
|
130661
130957
|
};
|
|
130662
130958
|
});
|
|
130663
130959
|
return { config: {
|
|
@@ -130679,12 +130975,13 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130679
130975
|
const tokenHash = yield* hashToken(rawToken);
|
|
130680
130976
|
const timestamp = yield* currentTimeMillis;
|
|
130681
130977
|
yield* pruneDead(timestamp);
|
|
130682
|
-
return yield* SynchronizedRef.modify(state, ({ records, spawnedThreads }) => {
|
|
130978
|
+
return yield* SynchronizedRef.modify(state, ({ records, spawnedThreads, fusionRoles }) => {
|
|
130683
130979
|
const current = records;
|
|
130684
130980
|
const record = current.get(tokenHash);
|
|
130685
130981
|
if (!record) return [void 0, {
|
|
130686
130982
|
records: current,
|
|
130687
|
-
spawnedThreads
|
|
130983
|
+
spawnedThreads,
|
|
130984
|
+
fusionRoles
|
|
130688
130985
|
}];
|
|
130689
130986
|
const next = new Map(current);
|
|
130690
130987
|
next.set(tokenHash, {
|
|
@@ -130698,14 +130995,15 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130698
130995
|
watchThreadIds: /* @__PURE__ */ new Set([...record.scope.watchThreadIds ?? [], ...record.controlThreadIds])
|
|
130699
130996
|
}, {
|
|
130700
130997
|
records: next,
|
|
130701
|
-
spawnedThreads
|
|
130998
|
+
spawnedThreads,
|
|
130999
|
+
fusionRoles
|
|
130702
131000
|
}];
|
|
130703
131001
|
});
|
|
130704
131002
|
});
|
|
130705
131003
|
const touch = Effect.fn("McpSessionRegistry.touch")(function* (threadId) {
|
|
130706
131004
|
const timestamp = yield* currentTimeMillis;
|
|
130707
131005
|
yield* pruneDead(timestamp);
|
|
130708
|
-
yield* SynchronizedRef.update(state, ({ records, spawnedThreads }) => {
|
|
131006
|
+
yield* SynchronizedRef.update(state, ({ records, spawnedThreads, fusionRoles }) => {
|
|
130709
131007
|
const current = records;
|
|
130710
131008
|
const next = new Map(current);
|
|
130711
131009
|
for (const [tokenHash, record] of current) if (record.scope.threadId === threadId) next.set(tokenHash, {
|
|
@@ -130714,12 +131012,13 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130714
131012
|
});
|
|
130715
131013
|
return {
|
|
130716
131014
|
records: next,
|
|
130717
|
-
spawnedThreads
|
|
131015
|
+
spawnedThreads,
|
|
131016
|
+
fusionRoles
|
|
130718
131017
|
};
|
|
130719
131018
|
});
|
|
130720
131019
|
});
|
|
130721
131020
|
const grantWatchThread = Effect.fn("McpSessionRegistry.grantWatchThread")(function* ({ watcherThreadId, watchedThreadId }) {
|
|
130722
|
-
yield* SynchronizedRef.update(state, ({ records, spawnedThreads }) => {
|
|
131021
|
+
yield* SynchronizedRef.update(state, ({ records, spawnedThreads, fusionRoles }) => {
|
|
130723
131022
|
const next = new Map(records);
|
|
130724
131023
|
for (const [tokenHash, record] of records) {
|
|
130725
131024
|
if (record.scope.threadId !== watcherThreadId) continue;
|
|
@@ -130734,12 +131033,13 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130734
131033
|
}
|
|
130735
131034
|
return {
|
|
130736
131035
|
records: next,
|
|
130737
|
-
spawnedThreads
|
|
131036
|
+
spawnedThreads,
|
|
131037
|
+
fusionRoles
|
|
130738
131038
|
};
|
|
130739
131039
|
});
|
|
130740
131040
|
});
|
|
130741
131041
|
const revokeWatchThread = Effect.fn("McpSessionRegistry.revokeWatchThread")(function* ({ watcherThreadId, watchedThreadId }) {
|
|
130742
|
-
yield* SynchronizedRef.update(state, ({ records, spawnedThreads }) => {
|
|
131042
|
+
yield* SynchronizedRef.update(state, ({ records, spawnedThreads, fusionRoles }) => {
|
|
130743
131043
|
const next = new Map(records);
|
|
130744
131044
|
for (const [tokenHash, record] of records) {
|
|
130745
131045
|
if (record.scope.threadId !== watcherThreadId) continue;
|
|
@@ -130762,12 +131062,13 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130762
131062
|
}
|
|
130763
131063
|
return {
|
|
130764
131064
|
records: next,
|
|
130765
|
-
spawnedThreads
|
|
131065
|
+
spawnedThreads,
|
|
131066
|
+
fusionRoles
|
|
130766
131067
|
};
|
|
130767
131068
|
});
|
|
130768
131069
|
});
|
|
130769
131070
|
const grantAdviseThread = Effect.fn("McpSessionRegistry.grantAdviseThread")(function* ({ watcherThreadId, advisedThreadId }) {
|
|
130770
|
-
yield* SynchronizedRef.update(state, ({ records, spawnedThreads }) => {
|
|
131071
|
+
yield* SynchronizedRef.update(state, ({ records, spawnedThreads, fusionRoles }) => {
|
|
130771
131072
|
const next = new Map(records);
|
|
130772
131073
|
for (const [tokenHash, record] of records) {
|
|
130773
131074
|
if (record.scope.threadId !== watcherThreadId) continue;
|
|
@@ -130782,12 +131083,13 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130782
131083
|
}
|
|
130783
131084
|
return {
|
|
130784
131085
|
records: next,
|
|
130785
|
-
spawnedThreads
|
|
131086
|
+
spawnedThreads,
|
|
131087
|
+
fusionRoles
|
|
130786
131088
|
};
|
|
130787
131089
|
});
|
|
130788
131090
|
});
|
|
130789
131091
|
const revokeAdviseThread = Effect.fn("McpSessionRegistry.revokeAdviseThread")(function* ({ watcherThreadId, advisedThreadId }) {
|
|
130790
|
-
yield* SynchronizedRef.update(state, ({ records, spawnedThreads }) => {
|
|
131092
|
+
yield* SynchronizedRef.update(state, ({ records, spawnedThreads, fusionRoles }) => {
|
|
130791
131093
|
const next = new Map(records);
|
|
130792
131094
|
for (const [tokenHash, record] of records) {
|
|
130793
131095
|
if (record.scope.threadId !== watcherThreadId) continue;
|
|
@@ -130810,12 +131112,47 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130810
131112
|
}
|
|
130811
131113
|
return {
|
|
130812
131114
|
records: next,
|
|
130813
|
-
spawnedThreads
|
|
131115
|
+
spawnedThreads,
|
|
131116
|
+
fusionRoles
|
|
130814
131117
|
};
|
|
130815
131118
|
});
|
|
130816
131119
|
});
|
|
131120
|
+
/**
|
|
131121
|
+
* Rewrites one thread's half on every credential it holds, and remembers it.
|
|
131122
|
+
*
|
|
131123
|
+
* A pair is routinely made over a thread that is already running, so the
|
|
131124
|
+
* credential it is using was minted before the pair existed and carries no
|
|
131125
|
+
* role. Patching the live record is what closes that session's gate without
|
|
131126
|
+
* restarting it; the remembered value is what answers for a session that has
|
|
131127
|
+
* not started yet, or has not reached its `issue` call.
|
|
131128
|
+
*/
|
|
131129
|
+
const applyFusionRole = (threadId, role) => SynchronizedRef.update(state, ({ records, spawnedThreads, fusionRoles }) => {
|
|
131130
|
+
const next = new Map(records);
|
|
131131
|
+
for (const [tokenHash, record] of records) {
|
|
131132
|
+
if (record.scope.threadId !== threadId) continue;
|
|
131133
|
+
const { fusionRole: _previous, ...scopeWithoutRole } = record.scope;
|
|
131134
|
+
next.set(tokenHash, {
|
|
131135
|
+
...record,
|
|
131136
|
+
scope: role === null ? scopeWithoutRole : {
|
|
131137
|
+
...scopeWithoutRole,
|
|
131138
|
+
fusionRole: role
|
|
131139
|
+
}
|
|
131140
|
+
});
|
|
131141
|
+
}
|
|
131142
|
+
return {
|
|
131143
|
+
records: next,
|
|
131144
|
+
spawnedThreads,
|
|
131145
|
+
fusionRoles: new Map(fusionRoles).set(threadId, role)
|
|
131146
|
+
};
|
|
131147
|
+
});
|
|
131148
|
+
const setFusionRole = Effect.fn("McpSessionRegistry.setFusionRole")(function* (input) {
|
|
131149
|
+
yield* applyFusionRole(input.threadId, input.role);
|
|
131150
|
+
});
|
|
131151
|
+
const clearFusionRole = Effect.fn("McpSessionRegistry.clearFusionRole")(function* (input) {
|
|
131152
|
+
yield* applyFusionRole(input.threadId, null);
|
|
131153
|
+
});
|
|
130817
131154
|
const recordSpawnedThread = Effect.fn("McpSessionRegistry.recordSpawnedThread")(function* (input) {
|
|
130818
|
-
yield* SynchronizedRef.update(state, ({ records, spawnedThreads }) => {
|
|
131155
|
+
yield* SynchronizedRef.update(state, ({ records, spawnedThreads, fusionRoles }) => {
|
|
130819
131156
|
const next = new Map(records);
|
|
130820
131157
|
for (const [tokenHash, record] of records) if (record.scope.providerSessionId === input.providerSessionId) next.set(tokenHash, {
|
|
130821
131158
|
...record,
|
|
@@ -130823,7 +131160,8 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130823
131160
|
});
|
|
130824
131161
|
return {
|
|
130825
131162
|
records: next,
|
|
130826
|
-
spawnedThreads: new Map([...spawnedThreads, [input.threadId, input.mayCreateFusionPairs === true]])
|
|
131163
|
+
spawnedThreads: new Map([...spawnedThreads, [input.threadId, input.mayCreateFusionPairs === true]]),
|
|
131164
|
+
fusionRoles
|
|
130827
131165
|
};
|
|
130828
131166
|
});
|
|
130829
131167
|
});
|
|
@@ -130831,6 +131169,8 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
130831
131169
|
issue,
|
|
130832
131170
|
resolve,
|
|
130833
131171
|
touch,
|
|
131172
|
+
setFusionRole,
|
|
131173
|
+
clearFusionRole,
|
|
130834
131174
|
grantWatchThread,
|
|
130835
131175
|
revokeWatchThread,
|
|
130836
131176
|
grantAdviseThread,
|
|
@@ -130862,6 +131202,8 @@ const issueActiveMcpCredential = (request) => activeMcpSessionRegistry ? activeM
|
|
|
130862
131202
|
*/
|
|
130863
131203
|
const touchActiveMcpThread = (threadId) => activeMcpSessionRegistry ? activeMcpSessionRegistry.touch(threadId) : Effect.void;
|
|
130864
131204
|
const grantActiveMcpWatchThread = (input) => activeMcpSessionRegistry ? activeMcpSessionRegistry.grantWatchThread(input) : Effect.void;
|
|
131205
|
+
const setActiveMcpFusionRole = (input) => activeMcpSessionRegistry ? activeMcpSessionRegistry.setFusionRole(input) : Effect.void;
|
|
131206
|
+
const clearActiveMcpFusionRole = (input) => activeMcpSessionRegistry ? activeMcpSessionRegistry.clearFusionRole(input) : Effect.void;
|
|
130865
131207
|
const revokeActiveMcpWatchThread = (input) => activeMcpSessionRegistry ? activeMcpSessionRegistry.revokeWatchThread(input) : Effect.void;
|
|
130866
131208
|
const grantActiveMcpAdviseThread = (input) => activeMcpSessionRegistry ? activeMcpSessionRegistry.grantAdviseThread(input) : Effect.void;
|
|
130867
131209
|
const revokeActiveMcpAdviseThread = (input) => activeMcpSessionRegistry ? activeMcpSessionRegistry.revokeAdviseThread(input) : Effect.void;
|
|
@@ -130983,6 +131325,15 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
|
|
|
130983
131325
|
return Option.isSome(thread) ? resolveProjectAgentBrowserAccess(settings, thread.value.projectId) : false;
|
|
130984
131326
|
})), Effect.catch((cause) => Effect.logWarning("Could not read settings; withholding agent browser access.", { cause }).pipe(Effect.as(false))));
|
|
130985
131327
|
const computerUseAccess = yield* registry.getInstanceInfo(providerInstanceId).pipe(Effect.map((info) => info.driverKind !== "codex"), Effect.catch(() => Effect.succeed(false)));
|
|
131328
|
+
const fusionRole = Option.isSome(projectionQuery) ? yield* projectionQuery.value.getActiveThreadPairs().pipe(Effect.map((pairs) => {
|
|
131329
|
+
const pair = pairs.find((entry) => entry.watcherThreadId === threadId || entry.implementerThreadId === threadId);
|
|
131330
|
+
if (pair === void 0) return void 0;
|
|
131331
|
+
return pair.watcherThreadId === threadId ? "watcher" : "implementer";
|
|
131332
|
+
}), Effect.mapError((cause) => new ProviderValidationError({
|
|
131333
|
+
operation: "ProviderService.prepareMcpSession",
|
|
131334
|
+
issue: "Could not read thread pairs, so the session's Fusion role is unknown. Refusing to issue an MCP credential that would carry no role.",
|
|
131335
|
+
cause
|
|
131336
|
+
}))) : yield* Effect.logWarning("No projection service, so an MCP credential is issued with no Fusion role. A pair whose event this process never saw will be ungated.", { threadId }).pipe(Effect.as(void 0));
|
|
130986
131337
|
const threadTitle = Option.isSome(projectionQuery) ? yield* projectionQuery.value.getThreadShellById(threadId).pipe(Effect.map((thread) => Option.isSome(thread) ? thread.value.title : void 0), Effect.catch(() => Effect.succeed(void 0))) : void 0;
|
|
130987
131338
|
const credential = yield* issueActiveMcpCredential({
|
|
130988
131339
|
threadId,
|
|
@@ -130991,7 +131342,8 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
|
|
|
130991
131342
|
browserAccess,
|
|
130992
131343
|
computerUseAccess,
|
|
130993
131344
|
...watchThreadIds !== void 0 ? { watchThreadIds } : {},
|
|
130994
|
-
...adviseThreadIds !== void 0 ? { adviseThreadIds } : {}
|
|
131345
|
+
...adviseThreadIds !== void 0 ? { adviseThreadIds } : {},
|
|
131346
|
+
...fusionRole !== void 0 ? { fusionRole } : {}
|
|
130995
131347
|
});
|
|
130996
131348
|
if (credential) yield* Effect.sync(() => setMcpProviderSession(credential.config));
|
|
130997
131349
|
return credential;
|
|
@@ -131837,6 +132189,47 @@ const mcpInputSchema = (schema) => ({
|
|
|
131837
132189
|
...schema,
|
|
131838
132190
|
type: "object"
|
|
131839
132191
|
});
|
|
132192
|
+
const P4_TOOL_NAMES = new Set(P4CODE_MCP_TOOL_NAMES);
|
|
132193
|
+
/**
|
|
132194
|
+
* The server's own answer to "may this role call this tool", applied to every
|
|
132195
|
+
* registered P4 tool before its handler runs.
|
|
132196
|
+
*
|
|
132197
|
+
* The adapters also hide a role's forbidden tools, but that is a context
|
|
132198
|
+
* measure: it depends on a user setting and on the one adapter that can filter
|
|
132199
|
+
* a session's tool list. A caller that never saw the tool can still name it,
|
|
132200
|
+
* so the rule that matters is enforced here, where every provider's call
|
|
132201
|
+
* arrives and neither a setting nor a prompt can move it.
|
|
132202
|
+
*
|
|
132203
|
+
* Only P4's own tools are governed. A name outside the catalogue belongs to
|
|
132204
|
+
* another server sharing this registration helper - Computer Use is one - and
|
|
132205
|
+
* this policy has nothing to say about it.
|
|
132206
|
+
*/
|
|
132207
|
+
const enforceFusionRole = (toolName) => Effect.serviceOption(McpInvocationContext).pipe(Effect.map((invocation) => {
|
|
132208
|
+
if (!P4_TOOL_NAMES.has(toolName)) return null;
|
|
132209
|
+
const role = invocation._tag === "Some" ? invocation.value.fusionRole : void 0;
|
|
132210
|
+
if (role === void 0 || fusionRoleMayCallP4Tool(role, toolName)) return null;
|
|
132211
|
+
return role;
|
|
132212
|
+
}));
|
|
132213
|
+
/** The half's own word, so the message names the caller rather than a code. */
|
|
132214
|
+
const ROLE_LABEL = {
|
|
132215
|
+
implementer: "builder",
|
|
132216
|
+
watcher: "supervisor"
|
|
132217
|
+
};
|
|
132218
|
+
const deniedResult = (toolName, role) => new McpSchema.CallToolResult({
|
|
132219
|
+
isError: true,
|
|
132220
|
+
content: [{
|
|
132221
|
+
type: "text",
|
|
132222
|
+
text: `${toolName} is not a tool the ${ROLE_LABEL[role]} half of a Fusion pair may call. Report the missing tool rather than working around it.`
|
|
132223
|
+
}]
|
|
132224
|
+
});
|
|
132225
|
+
/**
|
|
132226
|
+
* Wraps one tool handler in the role gate.
|
|
132227
|
+
*
|
|
132228
|
+
* Exported because `addTool` is reachable from two places: this helper, and
|
|
132229
|
+
* the one tool registered by hand. A gate that only covered the helper would
|
|
132230
|
+
* leave that tool open, which is the kind of hole a policy is judged by.
|
|
132231
|
+
*/
|
|
132232
|
+
const withFusionRoleGate = (toolName, handle) => (payload) => enforceFusionRole(toolName).pipe(Effect.flatMap((deniedRole) => deniedRole === null ? handle(payload) : Effect.succeed(deniedResult(toolName, deniedRole))));
|
|
131840
132233
|
const mcpToolkit = (toolkit) => Layer.effectDiscard(Effect.gen(function* () {
|
|
131841
132234
|
const server = yield* McpServer.McpServer;
|
|
131842
132235
|
yield* McpServer.registerToolkit(toolkit).pipe(Effect.provideService(McpServer.McpServer, {
|
|
@@ -131846,7 +132239,8 @@ const mcpToolkit = (toolkit) => Layer.effectDiscard(Effect.gen(function* () {
|
|
|
131846
132239
|
tool: new McpSchema.Tool({
|
|
131847
132240
|
...options.tool,
|
|
131848
132241
|
inputSchema: mcpInputSchema(options.tool.inputSchema)
|
|
131849
|
-
})
|
|
132242
|
+
}),
|
|
132243
|
+
handle: withFusionRoleGate(options.tool.name, options.handle)
|
|
131850
132244
|
})
|
|
131851
132245
|
}));
|
|
131852
132246
|
}));
|
|
@@ -132135,13 +132529,13 @@ const registerScreenshot = Effect.fn("device.registerScreenshot")(function* () {
|
|
|
132135
132529
|
}
|
|
132136
132530
|
}),
|
|
132137
132531
|
annotations: DeviceScreenshotTool.annotations,
|
|
132138
|
-
handle: (payload) => Effect.withFiber((fiber) => built.handle("device_screenshot", payload).pipe(Stream.unwrap, Stream.run(Sink.last()), Effect.flatMap(Effect.fromOption), Effect.provideService(DeviceService, service), Effect.provideService(DeviceScreenshot, screenshots), Effect.provideService(McpInvocationContext, Context.getUnsafe(fiber.context, McpInvocationContext)), Effect.flatMap((result) => screenshotContent(result.encodedResult)), Effect.orElseSucceed(() => new McpSchema.CallToolResult({
|
|
132532
|
+
handle: withFusionRoleGate(DeviceScreenshotTool.name, (payload) => Effect.withFiber((fiber) => built.handle("device_screenshot", payload).pipe(Stream.unwrap, Stream.run(Sink.last()), Effect.flatMap(Effect.fromOption), Effect.provideService(DeviceService, service), Effect.provideService(DeviceScreenshot, screenshots), Effect.provideService(McpInvocationContext, Context.getUnsafe(fiber.context, McpInvocationContext)), Effect.flatMap((result) => screenshotContent(result.encodedResult)), Effect.orElseSucceed(() => new McpSchema.CallToolResult({
|
|
132139
132533
|
isError: true,
|
|
132140
132534
|
content: [{
|
|
132141
132535
|
type: "text",
|
|
132142
132536
|
text: "Device screenshot unavailable. Check the open session and refresh devices."
|
|
132143
132537
|
}]
|
|
132144
|
-
}))))
|
|
132538
|
+
})))))
|
|
132145
132539
|
});
|
|
132146
132540
|
});
|
|
132147
132541
|
const DeviceToolkitRegistrationLive = Layer.mergeAll(mcpToolkit(DeviceStandardToolkit), Layer.effectDiscard(registerScreenshot())).pipe(Layer.provide(DeviceToolkitHandlersLive));
|
|
@@ -132903,6 +133297,7 @@ const ThreadPairCreateTool = Tool.make("thread_pair_create", {
|
|
|
132903
133297
|
dependencies: [
|
|
132904
133298
|
McpInvocationContext,
|
|
132905
133299
|
OrchestrationEngineService,
|
|
133300
|
+
McpSessionRegistry,
|
|
132906
133301
|
Crypto.Crypto
|
|
132907
133302
|
]
|
|
132908
133303
|
}).annotate(Tool.Title, "Create a Fusion pair").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
|
|
@@ -133297,6 +133692,14 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
|
|
|
133297
133692
|
watcherThreadId: fusionPair.watcherThreadId,
|
|
133298
133693
|
createdAt
|
|
133299
133694
|
}, threadId);
|
|
133695
|
+
yield* registry.setFusionRole({
|
|
133696
|
+
threadId,
|
|
133697
|
+
role: "implementer"
|
|
133698
|
+
});
|
|
133699
|
+
yield* registry.setFusionRole({
|
|
133700
|
+
threadId: fusionPair.watcherThreadId,
|
|
133701
|
+
role: "watcher"
|
|
133702
|
+
});
|
|
133300
133703
|
}
|
|
133301
133704
|
yield* dispatchControl({
|
|
133302
133705
|
type: "thread.turn.start",
|
|
@@ -133337,6 +133740,15 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
|
|
|
133337
133740
|
continuationMessageId,
|
|
133338
133741
|
createdAt: DateTime.formatIso(yield* DateTime.now)
|
|
133339
133742
|
}, invocation.threadId);
|
|
133743
|
+
const registry = yield* McpSessionRegistry;
|
|
133744
|
+
yield* registry.setFusionRole({
|
|
133745
|
+
threadId: invocation.threadId,
|
|
133746
|
+
role: "implementer"
|
|
133747
|
+
});
|
|
133748
|
+
yield* registry.setFusionRole({
|
|
133749
|
+
threadId: watcherThreadId,
|
|
133750
|
+
role: "watcher"
|
|
133751
|
+
});
|
|
133340
133752
|
return {
|
|
133341
133753
|
pairId,
|
|
133342
133754
|
implementerThreadId: invocation.threadId,
|
|
@@ -134182,6 +134594,16 @@ const TypeSafeToolkitHandlersLive = TypeSafeToolkit.toLayer({ typesafe_ask: (inp
|
|
|
134182
134594
|
* appending beside it.
|
|
134183
134595
|
*/
|
|
134184
134596
|
const evidenceReadActivityId = (watchedThreadId, throughSequence) => EventId.make(`fusion-evidence:${watchedThreadId}:${throughSequence}`);
|
|
134597
|
+
/**
|
|
134598
|
+
* The key a raw read records under, when the supervisor paged the events itself.
|
|
134599
|
+
*
|
|
134600
|
+
* A raw read has no fixed bound to key on: the supervisor discovers where the
|
|
134601
|
+
* window ends by paging to it, so keying on the sequence it happens to have
|
|
134602
|
+
* reached would mint one row per page. The turn is the bound that already
|
|
134603
|
+
* exists, and it collapses the whole raw read into the single row a compact
|
|
134604
|
+
* review would have written.
|
|
134605
|
+
*/
|
|
134606
|
+
const rawEvidenceReadActivityId = (watchedThreadId, readerTurnId) => EventId.make(`fusion-evidence:${watchedThreadId}:turn:${readerTurnId}`);
|
|
134185
134607
|
/** One report per review, keyed like the read it reports on so a retry replaces. */
|
|
134186
134608
|
const evidenceReportActivityId = (watchedThreadId, throughSequence) => EventId.make(`fusion-evidence-summary:${watchedThreadId}:${throughSequence}`);
|
|
134187
134609
|
/**
|
|
@@ -134214,7 +134636,8 @@ const evidenceTiming = (readAt, closedAt) => {
|
|
|
134214
134636
|
const correlateEvidence = Effect.fn("fusionEvidenceReport.correlateEvidence")(function* (input) {
|
|
134215
134637
|
const projections = yield* ProjectionSnapshotQuery;
|
|
134216
134638
|
const latest = yield* projections.getLatestThreadContextWindow(input.readerThreadId).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
134217
|
-
const
|
|
134639
|
+
const readContext = (activityId) => projections.getEvidenceReadContext(activityId).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
134640
|
+
const read = yield* readContext(input.readActivityId).pipe(Effect.flatMap((row) => Option.isSome(row) || input.fallbackReadActivityId === void 0 ? Effect.succeed(row) : readContext(input.fallbackReadActivityId)));
|
|
134218
134641
|
const timing = Option.match(read, {
|
|
134219
134642
|
onNone: () => null,
|
|
134220
134643
|
onSome: (row) => evidenceTiming(row.readAt, input.closedAt)
|
|
@@ -134255,8 +134678,14 @@ const ThreadWatchEventsTool = Tool.make("thread_watch_events", {
|
|
|
134255
134678
|
parameters: ThreadWatchEventsInput,
|
|
134256
134679
|
success: ThreadWatchEventsResult,
|
|
134257
134680
|
failure: ThreadWatchToolError,
|
|
134258
|
-
dependencies: [
|
|
134259
|
-
|
|
134681
|
+
dependencies: [
|
|
134682
|
+
McpInvocationContext,
|
|
134683
|
+
ThreadEventStreamService,
|
|
134684
|
+
OrchestrationEngineService,
|
|
134685
|
+
ProjectionSnapshotQuery,
|
|
134686
|
+
Crypto.Crypto
|
|
134687
|
+
]
|
|
134688
|
+
}).annotate(Tool.Title, "Read watched thread events").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
|
|
134260
134689
|
const ThreadWatchReviewTool = Tool.make("thread_watch_review", {
|
|
134261
134690
|
description: "Read bounded compact review evidence for an explicitly granted thread. Combines assistant fragments by message identity; preserves user requirements, tool evidence, plans, errors and turn boundaries. Resume with nextAfterSequence and the same throughSequence until hasMore is false, even on empty pages. Message operations append or replace by identity across pages. Truncated evidence includes source sequence ranges for targeted recovery with thread_watch_events; do not treat an excerpt as full proof.",
|
|
134262
134691
|
parameters: ThreadWatchReviewInput,
|
|
@@ -134308,21 +134737,26 @@ const REPORT_HEADING = {
|
|
|
134308
134737
|
* for; losing the note that it happened is a smaller loss than losing the page,
|
|
134309
134738
|
* so a rejected dispatch is logged and the page is returned anyway.
|
|
134310
134739
|
*/
|
|
134311
|
-
const recordEvidenceRead = Effect.fn("mcp.watch.recordEvidenceRead")(function* (readerThreadId,
|
|
134740
|
+
const recordEvidenceRead = Effect.fn("mcp.watch.recordEvidenceRead")(function* (readerThreadId, window) {
|
|
134312
134741
|
const engine = yield* OrchestrationEngineService;
|
|
134313
134742
|
const crypto = yield* Crypto.Crypto;
|
|
134314
134743
|
const projections = yield* ProjectionSnapshotQuery;
|
|
134315
134744
|
const createdAt = DateTime.formatIso(yield* DateTime.now);
|
|
134316
|
-
const recorded = yield* projections.getEvidenceReadContext(
|
|
134745
|
+
const recorded = yield* projections.getEvidenceReadContext(window.activityId).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
134317
134746
|
const opened = Option.isSome(recorded);
|
|
134318
134747
|
const readCreatedAt = Option.match(recorded, {
|
|
134319
134748
|
onNone: () => createdAt,
|
|
134320
134749
|
onSome: (row) => row.readAt
|
|
134321
134750
|
});
|
|
134322
134751
|
const readAfterSequence = Option.match(recorded, {
|
|
134323
|
-
onNone: () => afterSequence,
|
|
134324
|
-
onSome: (row) => row.afterSequence ?? afterSequence
|
|
134752
|
+
onNone: () => window.afterSequence,
|
|
134753
|
+
onSome: (row) => row.afterSequence ?? window.afterSequence
|
|
134325
134754
|
});
|
|
134755
|
+
const readThroughSequence = Option.match(recorded, {
|
|
134756
|
+
onNone: () => window.throughSequence,
|
|
134757
|
+
onSome: (row) => row.throughSequence === null ? window.throughSequence : Math.max(row.throughSequence, window.throughSequence)
|
|
134758
|
+
});
|
|
134759
|
+
const activeTurnId = yield* projections.getActiveTurnId(readerThreadId).pipe(Effect.orElseSucceed(() => Option.none()), Effect.map(Option.getOrNull));
|
|
134326
134760
|
const usedTokensAtRead = opened ? Option.getOrNull(Option.map(recorded, (row) => row.usedTokensAtRead)) : yield* projections.getLatestThreadContextWindow(readerThreadId).pipe(Effect.map(Option.map((row) => row.usedTokens)), Effect.orElseSucceed(() => Option.none()), Effect.map(Option.getOrNull));
|
|
134327
134761
|
yield* engine.dispatch({
|
|
134328
134762
|
type: "thread.activity.append",
|
|
@@ -134330,28 +134764,40 @@ const recordEvidenceRead = Effect.fn("mcp.watch.recordEvidenceRead")(function* (
|
|
|
134330
134764
|
threadId: readerThreadId,
|
|
134331
134765
|
createdAt,
|
|
134332
134766
|
activity: {
|
|
134333
|
-
id:
|
|
134767
|
+
id: window.activityId,
|
|
134334
134768
|
createdAt: readCreatedAt,
|
|
134335
134769
|
tone: "info",
|
|
134336
134770
|
kind: "fusion.evidence.read",
|
|
134337
134771
|
summary: "Read builder evidence",
|
|
134338
134772
|
turnId: null,
|
|
134339
134773
|
payload: {
|
|
134340
|
-
watchedThreadId:
|
|
134774
|
+
watchedThreadId: window.watchedThreadId,
|
|
134341
134775
|
afterSequence: readAfterSequence,
|
|
134342
|
-
throughSequence:
|
|
134343
|
-
nextAfterSequence:
|
|
134344
|
-
hasMore:
|
|
134345
|
-
scannedEvents:
|
|
134776
|
+
throughSequence: readThroughSequence,
|
|
134777
|
+
nextAfterSequence: window.nextAfterSequence,
|
|
134778
|
+
hasMore: window.hasMore,
|
|
134779
|
+
scannedEvents: window.scannedEvents,
|
|
134780
|
+
...activeTurnId === null ? {} : { turnId: activeTurnId },
|
|
134781
|
+
...window.raw === true ? { raw: true } : {},
|
|
134346
134782
|
...usedTokensAtRead === null ? {} : { usedTokensAtRead }
|
|
134347
134783
|
}
|
|
134348
134784
|
}
|
|
134349
134785
|
}).pipe(Effect.catchCause((cause) => Effect.logWarning("could not record the supervisor's evidence read", {
|
|
134350
134786
|
readerThreadId,
|
|
134351
|
-
watchedThreadId:
|
|
134787
|
+
watchedThreadId: window.watchedThreadId,
|
|
134352
134788
|
cause
|
|
134353
134789
|
})));
|
|
134354
134790
|
});
|
|
134791
|
+
/**
|
|
134792
|
+
* Whether this reader is the watcher half of a live pair over this thread.
|
|
134793
|
+
*
|
|
134794
|
+
* `thread_watch_events` is the generic polling tool, so an orchestrator reading
|
|
134795
|
+
* a thread it spawned goes through it too. Only a Fusion watcher is reading
|
|
134796
|
+
* builder evidence, and only its work log should grow a row saying so.
|
|
134797
|
+
*/
|
|
134798
|
+
const watchesAsFusionSupervisor = Effect.fn("mcp.watch.watchesAsFusionSupervisor")(function* (readerThreadId, watchedThreadId) {
|
|
134799
|
+
return (yield* (yield* ProjectionSnapshotQuery).getActiveThreadPairs().pipe(Effect.orElseSucceed(() => []))).some((pair) => pair.watcherThreadId === readerThreadId && pair.implementerThreadId === watchedThreadId);
|
|
134800
|
+
});
|
|
134355
134801
|
/** Bounded so one report cannot dominate a thread's activity storage. */
|
|
134356
134802
|
const clip = (value, limit) => {
|
|
134357
134803
|
const trimmed = value?.trim();
|
|
@@ -134365,7 +134811,15 @@ const WatchToolkitHandlersLive = WatchToolkit.toLayer({
|
|
|
134365
134811
|
threadId: input.threadId,
|
|
134366
134812
|
detail: cause.message
|
|
134367
134813
|
})));
|
|
134368
|
-
yield* recordEvidenceRead(invocation.threadId,
|
|
134814
|
+
yield* recordEvidenceRead(invocation.threadId, {
|
|
134815
|
+
activityId: evidenceReadActivityId(page.threadId, page.throughSequence),
|
|
134816
|
+
watchedThreadId: page.threadId,
|
|
134817
|
+
afterSequence: input.afterSequence ?? 0,
|
|
134818
|
+
throughSequence: page.throughSequence,
|
|
134819
|
+
nextAfterSequence: page.nextAfterSequence,
|
|
134820
|
+
hasMore: page.hasMore,
|
|
134821
|
+
scannedEvents: page.scannedEvents
|
|
134822
|
+
});
|
|
134369
134823
|
return page;
|
|
134370
134824
|
}),
|
|
134371
134825
|
thread_evidence_report: (input) => Effect.gen(function* () {
|
|
@@ -134373,9 +134827,11 @@ const WatchToolkitHandlersLive = WatchToolkit.toLayer({
|
|
|
134373
134827
|
const engine = yield* OrchestrationEngineService;
|
|
134374
134828
|
const crypto = yield* Crypto.Crypto;
|
|
134375
134829
|
const createdAt = DateTime.formatIso(yield* DateTime.now);
|
|
134830
|
+
const activeTurnId = yield* (yield* ProjectionSnapshotQuery).getActiveTurnId(invocation.threadId).pipe(Effect.orElseSucceed(() => Option.none()), Effect.map(Option.getOrNull));
|
|
134376
134831
|
const { context, timing } = yield* correlateEvidence({
|
|
134377
134832
|
readerThreadId: invocation.threadId,
|
|
134378
134833
|
readActivityId: evidenceReadActivityId(input.threadId, input.throughSequence),
|
|
134834
|
+
...activeTurnId === null ? {} : { fallbackReadActivityId: rawEvidenceReadActivityId(input.threadId, activeTurnId) },
|
|
134379
134835
|
closedAt: createdAt
|
|
134380
134836
|
});
|
|
134381
134837
|
const summary = clip(input.summary, THREAD_EVIDENCE_REPORT_MAX_CHARS);
|
|
@@ -134416,7 +134872,7 @@ const WatchToolkitHandlersLive = WatchToolkit.toLayer({
|
|
|
134416
134872
|
};
|
|
134417
134873
|
}),
|
|
134418
134874
|
thread_watch_events: (input) => Effect.gen(function* () {
|
|
134419
|
-
yield* requireWatchCapability(input.threadId);
|
|
134875
|
+
const invocation = yield* requireWatchCapability(input.threadId);
|
|
134420
134876
|
const page = yield* (yield* ThreadEventStreamService).read({
|
|
134421
134877
|
threadId: input.threadId,
|
|
134422
134878
|
...input.afterSequence !== void 0 ? { afterSequence: input.afterSequence } : {},
|
|
@@ -134425,6 +134881,28 @@ const WatchToolkitHandlersLive = WatchToolkit.toLayer({
|
|
|
134425
134881
|
threadId: input.threadId,
|
|
134426
134882
|
detail: cause.message
|
|
134427
134883
|
})));
|
|
134884
|
+
const lastSequence = page.events.at(-1)?.sequence;
|
|
134885
|
+
if (lastSequence !== void 0 && (yield* watchesAsFusionSupervisor(invocation.threadId, input.threadId))) {
|
|
134886
|
+
const projections = yield* ProjectionSnapshotQuery;
|
|
134887
|
+
const turnId = yield* projections.getActiveTurnId(invocation.threadId).pipe(Effect.orElseSucceed(() => Option.none()), Effect.map(Option.getOrNull));
|
|
134888
|
+
if (turnId !== null) {
|
|
134889
|
+
const activityId = rawEvidenceReadActivityId(input.threadId, turnId);
|
|
134890
|
+
const opened = yield* projections.getLatestEvidenceRead(invocation.threadId).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
134891
|
+
if (!Option.match(opened, {
|
|
134892
|
+
onNone: () => false,
|
|
134893
|
+
onSome: (row) => row.turnId === turnId && row.watchedThreadId === input.threadId
|
|
134894
|
+
})) yield* recordEvidenceRead(invocation.threadId, {
|
|
134895
|
+
activityId,
|
|
134896
|
+
watchedThreadId: input.threadId,
|
|
134897
|
+
afterSequence: input.afterSequence ?? 0,
|
|
134898
|
+
throughSequence: lastSequence,
|
|
134899
|
+
nextAfterSequence: lastSequence,
|
|
134900
|
+
hasMore: page.hasMore,
|
|
134901
|
+
scannedEvents: page.events.length,
|
|
134902
|
+
raw: true
|
|
134903
|
+
});
|
|
134904
|
+
}
|
|
134905
|
+
}
|
|
134428
134906
|
return {
|
|
134429
134907
|
threadId: input.threadId,
|
|
134430
134908
|
events: page.events,
|
|
@@ -134502,7 +134980,7 @@ const registerPreviewSnapshot = Effect.fn("McpHttpServer.registerPreviewSnapshot
|
|
|
134502
134980
|
}
|
|
134503
134981
|
}),
|
|
134504
134982
|
annotations: tool.annotations,
|
|
134505
|
-
handle: (payload) => Effect.withFiber((fiber) => {
|
|
134983
|
+
handle: withFusionRoleGate(tool.name, (payload) => Effect.withFiber((fiber) => {
|
|
134506
134984
|
const invocation = Context.getUnsafe(fiber.context, McpInvocationContext);
|
|
134507
134985
|
return built.handle("preview_snapshot", payload).pipe(Stream.unwrap, Stream.run(Sink.last()), Effect.flatMap(Effect.fromOption), Effect.provideService(PreviewAutomationBroker, broker), Effect.provideService(McpInvocationContext, invocation), Effect.matchCauseEffect({
|
|
134508
134986
|
onFailure: previewSnapshotFailure,
|
|
@@ -134531,7 +135009,7 @@ const registerPreviewSnapshot = Effect.fn("McpHttpServer.registerPreviewSnapshot
|
|
|
134531
135009
|
}));
|
|
134532
135010
|
}
|
|
134533
135011
|
}));
|
|
134534
|
-
})
|
|
135012
|
+
}))
|
|
134535
135013
|
});
|
|
134536
135014
|
});
|
|
134537
135015
|
const PreviewStandardToolkitRegistrationLive = mcpToolkit(PreviewStandardToolkit).pipe(Layer.provide(PreviewStandardToolkitHandlersLive));
|
|
@@ -140039,24 +140517,12 @@ const make$3 = Effect.gen(function* () {
|
|
|
140039
140517
|
yield* processReview(pair, completion, turn);
|
|
140040
140518
|
}
|
|
140041
140519
|
});
|
|
140042
|
-
/**
|
|
140043
|
-
|
|
140044
|
-
* reporting on it.
|
|
140045
|
-
*
|
|
140046
|
-
* The row it writes is keyed exactly like the supervisor's own report, so a
|
|
140047
|
-
* later report replaces it rather than doubling it, and it carries the same
|
|
140048
|
-
* server-measured context and timing. Without it, a review that was never
|
|
140049
|
-
* reported is indistinguishable in stored history from one that was never
|
|
140050
|
-
* attempted.
|
|
140051
|
-
*/
|
|
140052
|
-
const recordUnreportedEvidence = Effect.fn("FusionWatcherReactor.recordUnreportedEvidence")(function* (pair, completion) {
|
|
140053
|
-
const read = yield* projectionSnapshotQuery.getLatestEvidenceRead(pair.watcherThreadId).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
140054
|
-
if (Option.isNone(read)) return;
|
|
140055
|
-
const latest = read.value;
|
|
140056
|
-
if (latest.hasReport || latest.watchedThreadId !== pair.implementerThreadId) return;
|
|
140520
|
+
/** Writes the row, once the caller has established the turn owed one. */
|
|
140521
|
+
const stampUnreported = Effect.fn("FusionWatcherReactor.stampUnreported")(function* (pair, completion, latest) {
|
|
140057
140522
|
const { context, timing } = yield* correlateEvidence({
|
|
140058
140523
|
readerThreadId: pair.watcherThreadId,
|
|
140059
140524
|
readActivityId: evidenceReadActivityId(latest.watchedThreadId, latest.throughSequence),
|
|
140525
|
+
...completion.payload.turnId === null ? {} : { fallbackReadActivityId: rawEvidenceReadActivityId(latest.watchedThreadId, completion.payload.turnId) },
|
|
140060
140526
|
closedAt: completion.occurredAt
|
|
140061
140527
|
});
|
|
140062
140528
|
yield* orchestrationEngine.dispatch({
|
|
@@ -140086,6 +140552,39 @@ const make$3 = Effect.gen(function* () {
|
|
|
140086
140552
|
}
|
|
140087
140553
|
});
|
|
140088
140554
|
});
|
|
140555
|
+
/**
|
|
140556
|
+
* Closes a supervisor turn that read the builder's evidence and ended without
|
|
140557
|
+
* reporting on it.
|
|
140558
|
+
*
|
|
140559
|
+
* The row it writes is keyed exactly like the supervisor's own report, so a
|
|
140560
|
+
* later report replaces it rather than doubling it, and it carries the same
|
|
140561
|
+
* server-measured context and timing. Without it, a review that was never
|
|
140562
|
+
* reported is indistinguishable in stored history from one that was never
|
|
140563
|
+
* attempted.
|
|
140564
|
+
*/
|
|
140565
|
+
const recordUnreportedEvidence = Effect.fn("FusionWatcherReactor.recordUnreportedEvidence")(function* (pair, completion, input) {
|
|
140566
|
+
const read = yield* projectionSnapshotQuery.getLatestEvidenceRead(pair.watcherThreadId).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
140567
|
+
if (Option.isNone(read)) return;
|
|
140568
|
+
const latest = read.value;
|
|
140569
|
+
if (latest.hasReport || latest.watchedThreadId !== pair.implementerThreadId) return;
|
|
140570
|
+
if (completion.payload.turnId === null) return;
|
|
140571
|
+
const turn = yield* projectionSnapshotQuery.getTurnStart(pair.watcherThreadId, completion.payload.turnId).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
140572
|
+
if (Option.isNone(turn)) return;
|
|
140573
|
+
const started = turn.value;
|
|
140574
|
+
if (started.threadId !== pair.watcherThreadId) return;
|
|
140575
|
+
const readAt = Date.parse(latest.readAt);
|
|
140576
|
+
if (latest.turnId === null) {
|
|
140577
|
+
if (input.replayed) return;
|
|
140578
|
+
if (started.startedAt !== null && readAt < Date.parse(started.startedAt)) return;
|
|
140579
|
+
if (readAt > Date.parse(completion.occurredAt)) return;
|
|
140580
|
+
} else if (latest.turnId !== completion.payload.turnId) return;
|
|
140581
|
+
const wakeAt = (prefix) => projectionSnapshotQuery.getLatestThreadMessageWithIdPrefix(pair.watcherThreadId, prefix).pipe(Effect.map(Option.flatMap((wake) => {
|
|
140582
|
+
const sentAt = Date.parse(wake.createdAt);
|
|
140583
|
+
return Number.isNaN(sentAt) || sentAt > readAt ? Option.none() : Option.some(sentAt);
|
|
140584
|
+
})), Effect.orElseSucceed(() => Option.none()));
|
|
140585
|
+
if (!(started.messageId === null ? yield* Effect.map(Effect.all([wakeAt("fusion-review:"), wakeAt("fusion-gate:")]), ([review, gate]) => Option.isSome(review) && (Option.isNone(gate) || gate.value < review.value)) : started.messageId.startsWith("fusion-review:"))) return;
|
|
140586
|
+
yield* stampUnreported(pair, completion, latest);
|
|
140587
|
+
});
|
|
140089
140588
|
const processCompletion = Effect.fn("FusionWatcherReactor.processCompletion")(function* (event) {
|
|
140090
140589
|
const activePairs = ((yield* projectionSnapshotQuery.getCommandReadModel()).threadPairs ?? []).filter((pair) => pair.detachedAt === null);
|
|
140091
140590
|
const failedPair = activePairs.find((pair) => pair.implementerThreadId === event.payload.threadId || pair.watcherThreadId === event.payload.threadId);
|
|
@@ -140101,7 +140600,7 @@ const make$3 = Effect.gen(function* () {
|
|
|
140101
140600
|
}
|
|
140102
140601
|
if (event.payload.state !== "completed") return;
|
|
140103
140602
|
const watcherPair = activePairs.find((pair) => pair.watcherThreadId === event.payload.threadId);
|
|
140104
|
-
if (watcherPair !== void 0) yield* recordUnreportedEvidence(watcherPair, event).pipe(Effect.catchCause((cause) => Effect.logWarning("could not record an unreported Fusion evidence read", {
|
|
140603
|
+
if (watcherPair !== void 0) yield* recordUnreportedEvidence(watcherPair, event, { replayed: event.sequence <= liveEventsAfterSequence }).pipe(Effect.catchCause((cause) => Effect.logWarning("could not record an unreported Fusion evidence read", {
|
|
140105
140604
|
pairId: watcherPair.id,
|
|
140106
140605
|
cause: Cause.pretty(cause)
|
|
140107
140606
|
})));
|
|
@@ -140186,6 +140685,14 @@ const make$3 = Effect.gen(function* () {
|
|
|
140186
140685
|
case "thread-pair.gate-advanced": return yield* processGateAdvanced(event);
|
|
140187
140686
|
case "thread-pair.gate-resolved": return yield* processGateResolved(event);
|
|
140188
140687
|
case "thread-pair.created":
|
|
140688
|
+
yield* setActiveMcpFusionRole({
|
|
140689
|
+
threadId: event.payload.watcherThreadId,
|
|
140690
|
+
role: "watcher"
|
|
140691
|
+
});
|
|
140692
|
+
yield* setActiveMcpFusionRole({
|
|
140693
|
+
threadId: event.payload.implementerThreadId,
|
|
140694
|
+
role: "implementer"
|
|
140695
|
+
});
|
|
140189
140696
|
yield* grantActiveMcpWatchThread({
|
|
140190
140697
|
watcherThreadId: event.payload.watcherThreadId,
|
|
140191
140698
|
watchedThreadId: event.payload.implementerThreadId
|
|
@@ -140207,6 +140714,8 @@ const make$3 = Effect.gen(function* () {
|
|
|
140207
140714
|
case "thread-pair.detached": {
|
|
140208
140715
|
const pair = ((yield* projectionSnapshotQuery.getCommandReadModel()).threadPairs ?? []).find((candidate) => candidate.id === event.payload.pairId);
|
|
140209
140716
|
if (pair === void 0) return;
|
|
140717
|
+
yield* clearActiveMcpFusionRole({ threadId: pair.watcherThreadId });
|
|
140718
|
+
yield* clearActiveMcpFusionRole({ threadId: pair.implementerThreadId });
|
|
140210
140719
|
yield* revokeActiveMcpWatchThread({
|
|
140211
140720
|
watcherThreadId: pair.watcherThreadId,
|
|
140212
140721
|
watchedThreadId: pair.implementerThreadId
|