@sjawhar/pi-legion-envoy 5.28.0 → 5.30.0
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/README.md +4 -3
- package/agents/plan-gap-analyst.md +52 -0
- package/agents/plan-reviewer.md +61 -0
- package/dist/envoy.js +216 -15
- package/dist/legion.js +166 -5
- package/dist/skills/dispatch/references/messages.md +13 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,9 +71,10 @@ at `dist/skills`. The manifest declares `omp.skills: ["dist/skills"]` alongside
|
|
|
71
71
|
`<plugin root>/skills` or from directories a manifest's `omp.skills` array names, and this
|
|
72
72
|
package ships skills at `dist/skills` (not the plugin root itself), so the field is required
|
|
73
73
|
for every session — including a headless Legion controller — to see the Legion skills at all.
|
|
74
|
-
The tarball also ships `agents/`, the task agents Legion's prompts dispatch (`oracle
|
|
75
|
-
reviewer's pair `thermonuclear-deep-review` and `thermonuclear-code-quality
|
|
76
|
-
which writes the implementer's code
|
|
74
|
+
The tarball also ships `agents/`, the task agents Legion's prompts dispatch (`oracle`; the
|
|
75
|
+
reviewer's pair `thermonuclear-deep-review` and `thermonuclear-code-quality`; `deep-worker`,
|
|
76
|
+
which writes the implementer's code; and the planner's checks, `plan-gap-analyst` before it drafts
|
|
77
|
+
and `plan-reviewer` after), which Oh My Pi discovers under any extension package root:
|
|
77
78
|
an installed plugin in a pane, the explicit `--extension` root in a Sandbox pod. Each declares its
|
|
78
79
|
model as a role the operator maps (`docs/kubernetes.md`, "Model roles"). The skills those agents and the
|
|
79
80
|
role prompts load (the pair's rubrics, the implementer's `ce-simplify-code`) ship in
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan-gap-analyst
|
|
3
|
+
description: |
|
|
4
|
+
Pre-planning gap analyst. Read-only. Use before drafting a plan: finds the hidden requirements,
|
|
5
|
+
ambiguities, and missing machine-checkable acceptance criteria an issue leaves unsaid, each with
|
|
6
|
+
what the plan must answer.
|
|
7
|
+
# @oracle is the deployment's `oracle` model role; the Go daemon's boot gate refuses to start unless the operator's settings give
|
|
8
|
+
# this agent a model, through modelRoles.oracle or a task.agentModelOverrides entry for it (docs/kubernetes.md, Operator configuration).
|
|
9
|
+
model: ["@oracle"]
|
|
10
|
+
tools: read, glob, grep, todo
|
|
11
|
+
# The planner waits for the analysis before it drafts; without this, Oh My Pi runs a task agent in
|
|
12
|
+
# the background whenever async jobs are enabled.
|
|
13
|
+
blocking: true
|
|
14
|
+
color: yellow
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
You read an issue before its plan exists and find what the issue leaves unsaid that would derail
|
|
18
|
+
the plan. You advise the planner; the planner owns the plan.
|
|
19
|
+
|
|
20
|
+
## What you look for
|
|
21
|
+
|
|
22
|
+
1. **Hidden requirements.** What the change needs but the issue does not state: a caller, contract,
|
|
23
|
+
migration, configuration, document, or consumer the change reaches; a behaviour the existing
|
|
24
|
+
code promises that the change must keep.
|
|
25
|
+
2. **Ambiguities.** A sentence with two readings that lead to different work. Name both readings
|
|
26
|
+
and what each would cost.
|
|
27
|
+
3. **Acceptance a machine cannot check.** A criterion with no command, test, or request whose
|
|
28
|
+
expected output decides it, or a requirement with no criterion at all.
|
|
29
|
+
|
|
30
|
+
Read the code and the repository's own documentation before you call something a gap. A gap the
|
|
31
|
+
issue, the code, or the documentation already answers is not a finding.
|
|
32
|
+
|
|
33
|
+
## What a finding contains
|
|
34
|
+
|
|
35
|
+
- **The gap**, quoting the issue's words where it has them.
|
|
36
|
+
- **The evidence**: the file and line, or the issue text, that shows it.
|
|
37
|
+
- **What the plan must answer**: the question or decision the plan has to settle and, for an
|
|
38
|
+
acceptance gap, a check a machine could run. Say whether the code settles it or only the
|
|
39
|
+
issue's owner can (a product choice).
|
|
40
|
+
|
|
41
|
+
Report the findings that change the plan, the most work-changing first. Style, edge cases the plan
|
|
42
|
+
can settle in passing, and the design you would have chosen are not findings. When nothing changes
|
|
43
|
+
the plan, say so in one line.
|
|
44
|
+
|
|
45
|
+
## Constraints
|
|
46
|
+
|
|
47
|
+
- **Read-only: you analyse; you do not implement.** Your file tools are read-only; do not ask for
|
|
48
|
+
others or route edits through other means. Your session also carries the Dispatch and Envoy
|
|
49
|
+
tools: read with them if you need to, but write nothing through them — no issue, comment, ask,
|
|
50
|
+
message, suggestion or document edit, and nothing sent or published.
|
|
51
|
+
- Cite what you read. Anything you did not read is an assumption and is written as one.
|
|
52
|
+
- Do not write the plan.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan-reviewer
|
|
3
|
+
description: |
|
|
4
|
+
Plan executability reviewer. Read-only. Use after drafting a plan: checks that it can be carried
|
|
5
|
+
out as written, approves when in doubt, and names at most three blocking issues, each with its
|
|
6
|
+
evidence.
|
|
7
|
+
# @review is the deployment's `review` model role; the Go daemon's boot gate refuses to start unless the operator's settings give
|
|
8
|
+
# this agent a model, through modelRoles.review or a task.agentModelOverrides entry for it (docs/kubernetes.md, Operator configuration).
|
|
9
|
+
model: ["@review"]
|
|
10
|
+
tools: read, glob, grep, todo
|
|
11
|
+
# The planner waits for each verdict before it revises or proceeds; without this, Oh My Pi runs a
|
|
12
|
+
# task agent in the background whenever async jobs are enabled.
|
|
13
|
+
blocking: true
|
|
14
|
+
color: magenta
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
You answer one question: can a capable engineer who has only this plan and the repository carry
|
|
18
|
+
it out without getting stuck? You find blockers, not improvements. You advise the planner; the
|
|
19
|
+
planner owns the plan.
|
|
20
|
+
|
|
21
|
+
## What you check
|
|
22
|
+
|
|
23
|
+
1. **References.** The files, functions, commands, and skills the plan names exist and hold what
|
|
24
|
+
the plan says. Read them; do not assume.
|
|
25
|
+
2. **A place to start.** Every task says where the work is and what changes, in an order that can
|
|
26
|
+
be followed.
|
|
27
|
+
3. **Contradictions.** No two steps contradict each other, and no step contradicts the issue's
|
|
28
|
+
acceptance criteria.
|
|
29
|
+
4. **Runnable checks.** Every acceptance criterion names the surface and the exact command that
|
|
30
|
+
decides it.
|
|
31
|
+
|
|
32
|
+
You do not judge whether the approach is the best one, style, naming, edge cases the engineer can
|
|
33
|
+
settle during the work, or anything you would simply do differently.
|
|
34
|
+
|
|
35
|
+
## Verdict
|
|
36
|
+
|
|
37
|
+
Approve when in doubt. A plan that is mostly clear is good enough. Reject only for a blocker: a
|
|
38
|
+
reference that does not exist or does not hold what the plan says (read to confirm), a task with
|
|
39
|
+
nowhere to start, a contradiction, or a criterion nothing can check.
|
|
40
|
+
|
|
41
|
+
A rejection names at most three blocking issues, the most severe first. Each gives:
|
|
42
|
+
|
|
43
|
+
- **The issue**: the exact task, step, or criterion.
|
|
44
|
+
- **The evidence**: the file and line you read, or the plan's own words.
|
|
45
|
+
- **What must change** for it to pass.
|
|
46
|
+
|
|
47
|
+
A revised plan is read again in full and judged fresh: an issue the revision resolved is not
|
|
48
|
+
raised again, and a second round is no place for requests that would not have blocked the first.
|
|
49
|
+
|
|
50
|
+
## Output
|
|
51
|
+
|
|
52
|
+
The first line is the verdict alone: `approved` or `rejected`. Then one or two sentences on why.
|
|
53
|
+
On `rejected`, the numbered blocking issues. Nothing else.
|
|
54
|
+
|
|
55
|
+
## Constraints
|
|
56
|
+
|
|
57
|
+
- **Read-only: you review; you do not implement.** Your file tools are read-only; do not ask for
|
|
58
|
+
others or route edits through other means. Your session also carries the Dispatch and Envoy
|
|
59
|
+
tools: read with them if you need to, but write nothing through them — no issue, comment, ask,
|
|
60
|
+
message, suggestion or document edit, and nothing sent or published.
|
|
61
|
+
- Do not rewrite the plan.
|
package/dist/envoy.js
CHANGED
|
@@ -29796,6 +29796,7 @@ var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
|
29796
29796
|
body: exports_external.string(),
|
|
29797
29797
|
target: exports_external.string(),
|
|
29798
29798
|
in_reply_to: exports_external.string().nullable(),
|
|
29799
|
+
broadcast_id: exports_external.string().nullish(),
|
|
29799
29800
|
deliveries: exports_external.array(exports_external.unknown()),
|
|
29800
29801
|
created_at: exports_external.string()
|
|
29801
29802
|
});
|
|
@@ -30477,6 +30478,8 @@ var EnvelopeSchema = exports_external.object({
|
|
|
30477
30478
|
// ../contracts/src/handoff-schema.ts
|
|
30478
30479
|
var HANDOFF_SCHEMA_VERSION = 1;
|
|
30479
30480
|
var HANDOFF_PHASES = ["architect", "plan", "implement", "test", "review"];
|
|
30481
|
+
var PLAN_REVIEW_MAX_ROUNDS = 3;
|
|
30482
|
+
var PLAN_REVIEW_VERDICTS = ["approved", "rejected", "failed"];
|
|
30480
30483
|
var isoTimestamp = exports_external.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
30481
30484
|
var handoffPhase = exports_external.enum(HANDOFF_PHASES);
|
|
30482
30485
|
var nonEmpty = exports_external.string().trim().min(1);
|
|
@@ -30513,6 +30516,16 @@ var requiredSkillsSchema = exports_external.object({
|
|
|
30513
30516
|
test: exports_external.array(exports_external.string()).optional(),
|
|
30514
30517
|
review: exports_external.array(exports_external.string()).optional()
|
|
30515
30518
|
}).passthrough().optional();
|
|
30519
|
+
var gapAnalysisSchema = exports_external.object({
|
|
30520
|
+
findings: exports_external.array(exports_external.object({ finding: exports_external.string(), answer: exports_external.string() }).passthrough()).optional(),
|
|
30521
|
+
error: exports_external.string().optional()
|
|
30522
|
+
}).passthrough().optional();
|
|
30523
|
+
var planReviewSchema = exports_external.object({
|
|
30524
|
+
verdict: exports_external.enum(PLAN_REVIEW_VERDICTS),
|
|
30525
|
+
rounds: exports_external.number(),
|
|
30526
|
+
remainingIssues: exports_external.array(exports_external.object({ issue: exports_external.string(), evidence: exports_external.string() }).passthrough()).optional(),
|
|
30527
|
+
error: exports_external.string().optional()
|
|
30528
|
+
}).passthrough().optional();
|
|
30516
30529
|
var planSchema = baseHandoffSchema.extend({
|
|
30517
30530
|
phase: exports_external.literal("plan"),
|
|
30518
30531
|
taskCount: exports_external.number().optional(),
|
|
@@ -30520,7 +30533,9 @@ var planSchema = baseHandoffSchema.extend({
|
|
|
30520
30533
|
routingHints: routingHintsSchema,
|
|
30521
30534
|
concerns: exports_external.array(exports_external.string()).optional(),
|
|
30522
30535
|
workflowRecommendation: exports_external.string().optional(),
|
|
30523
|
-
requiredSkills: requiredSkillsSchema
|
|
30536
|
+
requiredSkills: requiredSkillsSchema,
|
|
30537
|
+
gapAnalysis: gapAnalysisSchema,
|
|
30538
|
+
planReview: planReviewSchema
|
|
30524
30539
|
});
|
|
30525
30540
|
var implementSchema = baseHandoffSchema.extend({
|
|
30526
30541
|
phase: exports_external.literal("implement"),
|
|
@@ -30560,13 +30575,60 @@ var reviewSchema = baseHandoffSchema.extend({
|
|
|
30560
30575
|
verdict: exports_external.enum(["approved", "changes_requested"]).optional(),
|
|
30561
30576
|
keyFindings: exports_external.array(exports_external.object({ severity: exports_external.string(), file: exports_external.string(), description: exports_external.string() }).passthrough()).optional()
|
|
30562
30577
|
});
|
|
30563
|
-
var nonEmptySkillList = exports_external.array(
|
|
30578
|
+
var nonEmptySkillList = exports_external.array(nonEmpty).min(1);
|
|
30579
|
+
var recorded = (shape, whatToRecord) => exports_external.object(shape, {
|
|
30580
|
+
error: (issue2) => issue2.input === undefined ? `missing \u2014 record ${whatToRecord}` : undefined
|
|
30581
|
+
}).passthrough();
|
|
30582
|
+
var gapAnalysisWriteSchema = recorded({
|
|
30583
|
+
findings: exports_external.array(exports_external.object({ finding: nonEmpty, answer: nonEmpty }).passthrough()).optional(),
|
|
30584
|
+
error: nonEmpty.optional()
|
|
30585
|
+
}, "the gap analyst's `findings`, each with how the plan answers it (`[]` when it found none), or its failed call's `error`").refine((analysis) => analysis.findings === undefined !== (analysis.error === undefined), {
|
|
30586
|
+
message: "record exactly one of `findings` or the failed call's `error`"
|
|
30587
|
+
});
|
|
30588
|
+
var planReviewWriteSchema = recorded({
|
|
30589
|
+
verdict: exports_external.enum(PLAN_REVIEW_VERDICTS),
|
|
30590
|
+
rounds: exports_external.number().int().min(1).max(PLAN_REVIEW_MAX_ROUNDS),
|
|
30591
|
+
remainingIssues: exports_external.array(exports_external.object({ issue: nonEmpty, evidence: nonEmpty }).passthrough()).optional(),
|
|
30592
|
+
error: nonEmpty.optional()
|
|
30593
|
+
}, "the plan review's `verdict` and `rounds`, with `remainingIssues` when it was rejected or `error` when a review's call failed").superRefine((review, ctx) => {
|
|
30594
|
+
const remaining = review.remainingIssues?.length ?? 0;
|
|
30595
|
+
if (review.verdict === "rejected" && remaining === 0) {
|
|
30596
|
+
ctx.addIssue({
|
|
30597
|
+
code: "custom",
|
|
30598
|
+
path: ["remainingIssues"],
|
|
30599
|
+
message: "a rejected review records the blocking issues its last round named"
|
|
30600
|
+
});
|
|
30601
|
+
}
|
|
30602
|
+
if (review.verdict === "rejected" && review.rounds < PLAN_REVIEW_MAX_ROUNDS) {
|
|
30603
|
+
ctx.addIssue({
|
|
30604
|
+
code: "custom",
|
|
30605
|
+
path: ["rounds"],
|
|
30606
|
+
message: `a review still rejecting after ${review.rounds} of ${PLAN_REVIEW_MAX_ROUNDS} rounds is revised and reviewed again, not recorded`
|
|
30607
|
+
});
|
|
30608
|
+
}
|
|
30609
|
+
if (review.verdict === "approved" && remaining > 0) {
|
|
30610
|
+
ctx.addIssue({
|
|
30611
|
+
code: "custom",
|
|
30612
|
+
path: ["remainingIssues"],
|
|
30613
|
+
message: "an approved review leaves no blocking issue standing"
|
|
30614
|
+
});
|
|
30615
|
+
}
|
|
30616
|
+
if (review.verdict === "failed" !== (review.error !== undefined)) {
|
|
30617
|
+
ctx.addIssue({
|
|
30618
|
+
code: "custom",
|
|
30619
|
+
path: ["error"],
|
|
30620
|
+
message: "a failed review records its call's error, and only a failed review does"
|
|
30621
|
+
});
|
|
30622
|
+
}
|
|
30623
|
+
});
|
|
30564
30624
|
var planWriteSchema = planSchema.extend({
|
|
30565
30625
|
requiredSkills: exports_external.object({
|
|
30566
30626
|
implement: nonEmptySkillList,
|
|
30567
30627
|
test: nonEmptySkillList,
|
|
30568
30628
|
review: nonEmptySkillList
|
|
30569
|
-
}).passthrough()
|
|
30629
|
+
}).passthrough(),
|
|
30630
|
+
gapAnalysis: gapAnalysisWriteSchema,
|
|
30631
|
+
planReview: planReviewWriteSchema
|
|
30570
30632
|
});
|
|
30571
30633
|
var phaseHandoffSchema = exports_external.discriminatedUnion("phase", [
|
|
30572
30634
|
architectSchema,
|
|
@@ -31969,7 +32031,8 @@ function renderInbound(raw, sessionID, subject2) {
|
|
|
31969
32031
|
replyPath: dispatchReplyPath("message", message2.data.id),
|
|
31970
32032
|
replyFields: {},
|
|
31971
32033
|
issueKey: frame.event.issue_key,
|
|
31972
|
-
body: message2.data.body
|
|
32034
|
+
body: message2.data.body,
|
|
32035
|
+
...typeof message2.data.broadcast_id === "string" ? { broadcastId: message2.data.broadcast_id } : {}
|
|
31973
32036
|
};
|
|
31974
32037
|
dispatchReply = {
|
|
31975
32038
|
tool: "dispatch_message",
|
|
@@ -32517,6 +32580,9 @@ class DispatchClient {
|
|
|
32517
32580
|
async getMessageThread(id, session) {
|
|
32518
32581
|
return this.#json("GET", ["api", "v1", "messages", id], undefined, { session });
|
|
32519
32582
|
}
|
|
32583
|
+
async acceptMessageDelivery(id, attempt, input) {
|
|
32584
|
+
return this.#json("POST", ["api", "v1", "messages", id, "deliveries", String(attempt), "accept"], input);
|
|
32585
|
+
}
|
|
32520
32586
|
async artifact(issue2, input) {
|
|
32521
32587
|
const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"];
|
|
32522
32588
|
if ("content" in input)
|
|
@@ -35236,7 +35302,7 @@ function conversationParts(content) {
|
|
|
35236
35302
|
return parts;
|
|
35237
35303
|
}
|
|
35238
35304
|
function frameFor(entry) {
|
|
35239
|
-
const { message } = entry;
|
|
35305
|
+
const { dispatchMessageId, message } = entry;
|
|
35240
35306
|
if (message.role === "toolResult" && message.toolCallId !== undefined) {
|
|
35241
35307
|
return {
|
|
35242
35308
|
kind: "tool-result",
|
|
@@ -35258,7 +35324,8 @@ function frameFor(entry) {
|
|
|
35258
35324
|
id: entry.key.slice(2),
|
|
35259
35325
|
parts: conversationParts(message.host.content),
|
|
35260
35326
|
role: message.role === "assistant" ? "assistant" : "user",
|
|
35261
|
-
streaming: entry.streaming
|
|
35327
|
+
streaming: entry.streaming,
|
|
35328
|
+
...dispatchMessageId === undefined ? {} : { dispatchMessageId }
|
|
35262
35329
|
},
|
|
35263
35330
|
seq: entry.seq,
|
|
35264
35331
|
v: AGENT_STREAM_PROTOCOL
|
|
@@ -35287,7 +35354,7 @@ class AgentStreamPublisher {
|
|
|
35287
35354
|
this.#publishedAt.clear();
|
|
35288
35355
|
this.#watchedUntil = 0;
|
|
35289
35356
|
}
|
|
35290
|
-
record(subject2, raw, streaming) {
|
|
35357
|
+
record(subject2, raw, streaming, dispatchMessageId) {
|
|
35291
35358
|
const message = readMessage(raw);
|
|
35292
35359
|
if (message === null)
|
|
35293
35360
|
return false;
|
|
@@ -35300,6 +35367,7 @@ class AgentStreamPublisher {
|
|
|
35300
35367
|
this.#seq += 1;
|
|
35301
35368
|
const entry = {
|
|
35302
35369
|
at: message.timestamp,
|
|
35370
|
+
dispatchMessageId: dispatchMessageId ?? this.#history.get(key)?.dispatchMessageId,
|
|
35303
35371
|
key,
|
|
35304
35372
|
message,
|
|
35305
35373
|
seq: this.#seq,
|
|
@@ -35370,6 +35438,101 @@ function withDispatchFirst(messages, context) {
|
|
|
35370
35438
|
];
|
|
35371
35439
|
}
|
|
35372
35440
|
|
|
35441
|
+
// src/dispatch-user-turn.ts
|
|
35442
|
+
function isUserTurnCandidate(rendered) {
|
|
35443
|
+
const { delivery } = rendered;
|
|
35444
|
+
return rendered.envelope?.source === "dispatch" && delivery?.resource === "message" && delivery.issueKey === null && delivery.broadcastId === undefined && rendered.dispatchActor?.kind === "user" && (delivery.mode === "aside" || delivery.mode === "steer");
|
|
35445
|
+
}
|
|
35446
|
+
var AcceptedDeliverySchema = exports_external.object({
|
|
35447
|
+
message_id: exports_external.string(),
|
|
35448
|
+
body: exports_external.string(),
|
|
35449
|
+
delivery: exports_external.enum(["aside", "steer"])
|
|
35450
|
+
});
|
|
35451
|
+
function turnFromAccept(accepted) {
|
|
35452
|
+
const parsed = AcceptedDeliverySchema.safeParse(accepted);
|
|
35453
|
+
if (!parsed.success)
|
|
35454
|
+
return;
|
|
35455
|
+
return { body: parsed.data.body, messageId: parsed.data.message_id, mode: parsed.data.delivery };
|
|
35456
|
+
}
|
|
35457
|
+
var HANDLED_ATTEMPT_ENTRY = "envoy-dispatch-handled-attempt";
|
|
35458
|
+
function handledAttemptKey(messageId, attempt) {
|
|
35459
|
+
return `${messageId}#${attempt}`;
|
|
35460
|
+
}
|
|
35461
|
+
function handledAttempts(entries) {
|
|
35462
|
+
const handled = new Set;
|
|
35463
|
+
for (const entry of entries) {
|
|
35464
|
+
if (typeof entry !== "object" || entry === null)
|
|
35465
|
+
continue;
|
|
35466
|
+
if (!("type" in entry) || entry.type !== "custom")
|
|
35467
|
+
continue;
|
|
35468
|
+
if (!("customType" in entry) || entry.customType !== HANDLED_ATTEMPT_ENTRY)
|
|
35469
|
+
continue;
|
|
35470
|
+
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
35471
|
+
continue;
|
|
35472
|
+
const { data } = entry;
|
|
35473
|
+
if (!("message_id" in data) || typeof data.message_id !== "string")
|
|
35474
|
+
continue;
|
|
35475
|
+
if (!("attempt" in data) || typeof data.attempt !== "number")
|
|
35476
|
+
continue;
|
|
35477
|
+
handled.add(handledAttemptKey(data.message_id, data.attempt));
|
|
35478
|
+
}
|
|
35479
|
+
return handled;
|
|
35480
|
+
}
|
|
35481
|
+
function userMessageText(content) {
|
|
35482
|
+
if (typeof content === "string")
|
|
35483
|
+
return content;
|
|
35484
|
+
if (!Array.isArray(content))
|
|
35485
|
+
return "";
|
|
35486
|
+
const pieces = [];
|
|
35487
|
+
for (const part of content) {
|
|
35488
|
+
if (typeof part !== "object" || part === null)
|
|
35489
|
+
continue;
|
|
35490
|
+
if ("type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
|
|
35491
|
+
pieces.push(part.text);
|
|
35492
|
+
}
|
|
35493
|
+
}
|
|
35494
|
+
return pieces.join(`
|
|
35495
|
+
`);
|
|
35496
|
+
}
|
|
35497
|
+
var INJECTED_USER_TURNS = Symbol.for("legion.pi-envoy.injected-user-turns");
|
|
35498
|
+
function injectedUserTurns() {
|
|
35499
|
+
const store = globalThis;
|
|
35500
|
+
const turns = store[INJECTED_USER_TURNS] ?? new Map;
|
|
35501
|
+
store[INJECTED_USER_TURNS] = turns;
|
|
35502
|
+
return turns;
|
|
35503
|
+
}
|
|
35504
|
+
function noteInjectedUserTurn(sessionID, body, messageId) {
|
|
35505
|
+
const turns = injectedUserTurns();
|
|
35506
|
+
const session = turns.get(sessionID) ?? { found: new Map, sent: [] };
|
|
35507
|
+
session.sent.push({ body, messageId });
|
|
35508
|
+
turns.set(sessionID, session);
|
|
35509
|
+
}
|
|
35510
|
+
function matchInjectedUserTurn(sessionID, message) {
|
|
35511
|
+
if (typeof message !== "object" || message === null)
|
|
35512
|
+
return;
|
|
35513
|
+
if (!("role" in message) || message.role !== "user")
|
|
35514
|
+
return;
|
|
35515
|
+
if (!("timestamp" in message) || typeof message.timestamp !== "number")
|
|
35516
|
+
return;
|
|
35517
|
+
const session = injectedUserTurns().get(sessionID);
|
|
35518
|
+
if (session === undefined)
|
|
35519
|
+
return;
|
|
35520
|
+
const known = session.found.get(message.timestamp);
|
|
35521
|
+
if (known !== undefined)
|
|
35522
|
+
return known;
|
|
35523
|
+
const text = userMessageText("content" in message ? message.content : undefined);
|
|
35524
|
+
const index = session.sent.findIndex((sent2) => sent2.body === text);
|
|
35525
|
+
const sent = session.sent[index];
|
|
35526
|
+
if (sent === undefined)
|
|
35527
|
+
return;
|
|
35528
|
+
session.sent.splice(index, 1);
|
|
35529
|
+
session.found.set(message.timestamp, sent.messageId);
|
|
35530
|
+
return sent.messageId;
|
|
35531
|
+
}
|
|
35532
|
+
function endInjectedUserTurns(sessionID) {
|
|
35533
|
+
injectedUserTurns().delete(sessionID);
|
|
35534
|
+
}
|
|
35535
|
+
|
|
35373
35536
|
// src/envoy-session.ts
|
|
35374
35537
|
import path2 from "path";
|
|
35375
35538
|
var ENVOY_SESSIONS = Symbol.for("legion.pi-envoy.envoy-session");
|
|
@@ -35400,9 +35563,9 @@ function resolveEnvoySession(sessionFile) {
|
|
|
35400
35563
|
const parent = `${directory}.jsonl`;
|
|
35401
35564
|
if (parent === candidate)
|
|
35402
35565
|
break;
|
|
35403
|
-
const
|
|
35404
|
-
if (
|
|
35405
|
-
return
|
|
35566
|
+
const recorded2 = sessions.get(parent);
|
|
35567
|
+
if (recorded2 !== undefined)
|
|
35568
|
+
return recorded2;
|
|
35406
35569
|
if (path2.dirname(directory) === directory)
|
|
35407
35570
|
break;
|
|
35408
35571
|
candidate = parent;
|
|
@@ -35626,6 +35789,7 @@ var NATS_RETRY_INTERVAL_MS = 15000;
|
|
|
35626
35789
|
var CAPABILITIES_WITHOUT_BTW = DELIVERY_CAPABILITIES.filter((capability) => capability !== "btw");
|
|
35627
35790
|
var ROLE_CLAIM_ENTRY = "envoy-role-claim";
|
|
35628
35791
|
var OPEN_ASKS_TIMEOUT_MS = 3000;
|
|
35792
|
+
var USER_TURN_ACCEPT_TIMEOUT_MS = 1e4;
|
|
35629
35793
|
var LEGION_MANAGED_ENTRY = "legion-managed-session";
|
|
35630
35794
|
var ASK_REMINDER_MESSAGE = "dispatch-ask-reminder";
|
|
35631
35795
|
var ASK_PROMPT_LINE_SEPARATOR = /\r\n|[\r\n\u0085\u2028\u2029]/;
|
|
@@ -35755,6 +35919,7 @@ function envoyExtension(pi) {
|
|
|
35755
35919
|
let runSeq = 0;
|
|
35756
35920
|
let legionManagedTranscript = false;
|
|
35757
35921
|
const legionManaged = (id) => legionManagedTranscript || legionRoleClaimBridge().managedSessions.has(id);
|
|
35922
|
+
let handledDispatchAttempts = new Set;
|
|
35758
35923
|
const availabilityWarningSessionIDs = new Set;
|
|
35759
35924
|
const warnAskAvailability = (context, error48) => {
|
|
35760
35925
|
const sessionID2 = context.sessionManager.getSessionId();
|
|
@@ -35812,6 +35977,7 @@ function envoyExtension(pi) {
|
|
|
35812
35977
|
awarenessGeneration++;
|
|
35813
35978
|
}
|
|
35814
35979
|
legionManagedTranscript = branch.some(isLegionManagedEntry);
|
|
35980
|
+
handledDispatchAttempts = handledAttempts(context.sessionManager.getEntries());
|
|
35815
35981
|
};
|
|
35816
35982
|
pi.on("resources_discover", async () => ({ skillPaths: [SKILLS_DIRECTORY] }));
|
|
35817
35983
|
registerEnvoyMessageRenderer(pi);
|
|
@@ -35831,6 +35997,34 @@ function envoyExtension(pi) {
|
|
|
35831
35997
|
const postDispatchReply = async (delivery, result) => {
|
|
35832
35998
|
await postDeliveryReply(currentDispatchConfig(), sessionID, delivery, result);
|
|
35833
35999
|
};
|
|
36000
|
+
const acceptedUserTurn = async (rendered) => {
|
|
36001
|
+
if (!isUserTurnCandidate(rendered))
|
|
36002
|
+
return;
|
|
36003
|
+
const { delivery } = rendered;
|
|
36004
|
+
const key = handledAttemptKey(delivery.id, delivery.attempt);
|
|
36005
|
+
if (handledDispatchAttempts.has(key)) {
|
|
36006
|
+
logger.warn("envoy: a frame names a Dispatch attempt this session already delivered; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id });
|
|
36007
|
+
return;
|
|
36008
|
+
}
|
|
36009
|
+
handledDispatchAttempts.add(key);
|
|
36010
|
+
pi.appendEntry(HANDLED_ATTEMPT_ENTRY, { attempt: delivery.attempt, message_id: delivery.id });
|
|
36011
|
+
try {
|
|
36012
|
+
const config2 = activeDispatchConfig(process.env, { cwd: process.cwd() });
|
|
36013
|
+
if (config2 === null)
|
|
36014
|
+
return;
|
|
36015
|
+
const accepted = await new DispatchClient(config2.url, config2.token, fetch, AbortSignal.timeout(USER_TURN_ACCEPT_TIMEOUT_MS)).acceptMessageDelivery(delivery.id, delivery.attempt, {
|
|
36016
|
+
actor: { id: sessionID, kind: "session" }
|
|
36017
|
+
});
|
|
36018
|
+
const turn = turnFromAccept(accepted);
|
|
36019
|
+
if (turn === undefined) {
|
|
36020
|
+
logger.warn("envoy: Dispatch accepted a direct message without the stored body and mode to inject; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id });
|
|
36021
|
+
}
|
|
36022
|
+
return turn;
|
|
36023
|
+
} catch (error48) {
|
|
36024
|
+
logger.warn("envoy: Dispatch did not accept a direct message as this session's turn; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id, error: messageFor(error48) });
|
|
36025
|
+
return;
|
|
36026
|
+
}
|
|
36027
|
+
};
|
|
35834
36028
|
const deliver = async (subject2, raw, reply) => {
|
|
35835
36029
|
const rendered = renderInbound(raw, sessionID, subject2);
|
|
35836
36030
|
const directSubject = agentSubject(sessionID);
|
|
@@ -35889,10 +36083,16 @@ function envoyExtension(pi) {
|
|
|
35889
36083
|
}
|
|
35890
36084
|
}
|
|
35891
36085
|
} else {
|
|
35892
|
-
|
|
35893
|
-
|
|
35894
|
-
|
|
35895
|
-
|
|
36086
|
+
const turn = await acceptedUserTurn(rendered);
|
|
36087
|
+
if (turn === undefined) {
|
|
36088
|
+
pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, {
|
|
36089
|
+
deliverAs: rendered.delivery?.mode === "aside" ? "aside" : "steer",
|
|
36090
|
+
triggerTurn: true
|
|
36091
|
+
});
|
|
36092
|
+
} else {
|
|
36093
|
+
noteInjectedUserTurn(sessionID, turn.body, turn.messageId);
|
|
36094
|
+
pi.sendUserMessage(turn.body, turn.mode === "aside" ? { deliverAs: "aside" } : undefined);
|
|
36095
|
+
}
|
|
35896
36096
|
}
|
|
35897
36097
|
} catch (error48) {
|
|
35898
36098
|
console.warn(`[envoy] failed to deliver envelope ${envelope2?.event_id ?? "unknown"}`, error48);
|
|
@@ -36018,7 +36218,7 @@ function envoyExtension(pi) {
|
|
|
36018
36218
|
const recordAgentStreamMessage = (message, streaming) => {
|
|
36019
36219
|
if (sessionID === "")
|
|
36020
36220
|
return;
|
|
36021
|
-
agentStream.record(agentStreamFramesSubject(sessionID), message, streaming);
|
|
36221
|
+
agentStream.record(agentStreamFramesSubject(sessionID), message, streaming, matchInjectedUserTurn(sessionID, message));
|
|
36022
36222
|
};
|
|
36023
36223
|
pi.on("message_start", async (event) => {
|
|
36024
36224
|
recordAgentStreamMessage(event.message, true);
|
|
@@ -36495,6 +36695,7 @@ function envoyExtension(pi) {
|
|
|
36495
36695
|
}
|
|
36496
36696
|
pi.on("agent_end", async (event, context) => {
|
|
36497
36697
|
const id = context.sessionManager.getSessionId();
|
|
36698
|
+
endInjectedUserTurns(sessionID);
|
|
36498
36699
|
const lastReply = event.messages?.findLast((message) => message.role === "assistant");
|
|
36499
36700
|
const ask = sideTurn(pi, context);
|
|
36500
36701
|
if (event.willContinue === true || lastReply?.stopReason !== "stop" || !context.hasUI || id === "" || ask === undefined || !checkOwed(id)) {
|
package/dist/legion.js
CHANGED
|
@@ -29965,6 +29965,7 @@ var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
|
29965
29965
|
body: exports_external.string(),
|
|
29966
29966
|
target: exports_external.string(),
|
|
29967
29967
|
in_reply_to: exports_external.string().nullable(),
|
|
29968
|
+
broadcast_id: exports_external.string().nullish(),
|
|
29968
29969
|
deliveries: exports_external.array(exports_external.unknown()),
|
|
29969
29970
|
created_at: exports_external.string()
|
|
29970
29971
|
});
|
|
@@ -30646,6 +30647,8 @@ var EnvelopeSchema = exports_external.object({
|
|
|
30646
30647
|
// ../contracts/src/handoff-schema.ts
|
|
30647
30648
|
var HANDOFF_SCHEMA_VERSION = 1;
|
|
30648
30649
|
var HANDOFF_PHASES = ["architect", "plan", "implement", "test", "review"];
|
|
30650
|
+
var PLAN_REVIEW_MAX_ROUNDS = 3;
|
|
30651
|
+
var PLAN_REVIEW_VERDICTS = ["approved", "rejected", "failed"];
|
|
30649
30652
|
var isoTimestamp = exports_external.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
30650
30653
|
var handoffPhase = exports_external.enum(HANDOFF_PHASES);
|
|
30651
30654
|
var nonEmpty = exports_external.string().trim().min(1);
|
|
@@ -30682,6 +30685,16 @@ var requiredSkillsSchema = exports_external.object({
|
|
|
30682
30685
|
test: exports_external.array(exports_external.string()).optional(),
|
|
30683
30686
|
review: exports_external.array(exports_external.string()).optional()
|
|
30684
30687
|
}).passthrough().optional();
|
|
30688
|
+
var gapAnalysisSchema = exports_external.object({
|
|
30689
|
+
findings: exports_external.array(exports_external.object({ finding: exports_external.string(), answer: exports_external.string() }).passthrough()).optional(),
|
|
30690
|
+
error: exports_external.string().optional()
|
|
30691
|
+
}).passthrough().optional();
|
|
30692
|
+
var planReviewSchema = exports_external.object({
|
|
30693
|
+
verdict: exports_external.enum(PLAN_REVIEW_VERDICTS),
|
|
30694
|
+
rounds: exports_external.number(),
|
|
30695
|
+
remainingIssues: exports_external.array(exports_external.object({ issue: exports_external.string(), evidence: exports_external.string() }).passthrough()).optional(),
|
|
30696
|
+
error: exports_external.string().optional()
|
|
30697
|
+
}).passthrough().optional();
|
|
30685
30698
|
var planSchema = baseHandoffSchema.extend({
|
|
30686
30699
|
phase: exports_external.literal("plan"),
|
|
30687
30700
|
taskCount: exports_external.number().optional(),
|
|
@@ -30689,7 +30702,9 @@ var planSchema = baseHandoffSchema.extend({
|
|
|
30689
30702
|
routingHints: routingHintsSchema,
|
|
30690
30703
|
concerns: exports_external.array(exports_external.string()).optional(),
|
|
30691
30704
|
workflowRecommendation: exports_external.string().optional(),
|
|
30692
|
-
requiredSkills: requiredSkillsSchema
|
|
30705
|
+
requiredSkills: requiredSkillsSchema,
|
|
30706
|
+
gapAnalysis: gapAnalysisSchema,
|
|
30707
|
+
planReview: planReviewSchema
|
|
30693
30708
|
});
|
|
30694
30709
|
var implementSchema = baseHandoffSchema.extend({
|
|
30695
30710
|
phase: exports_external.literal("implement"),
|
|
@@ -30729,13 +30744,60 @@ var reviewSchema = baseHandoffSchema.extend({
|
|
|
30729
30744
|
verdict: exports_external.enum(["approved", "changes_requested"]).optional(),
|
|
30730
30745
|
keyFindings: exports_external.array(exports_external.object({ severity: exports_external.string(), file: exports_external.string(), description: exports_external.string() }).passthrough()).optional()
|
|
30731
30746
|
});
|
|
30732
|
-
var nonEmptySkillList = exports_external.array(
|
|
30747
|
+
var nonEmptySkillList = exports_external.array(nonEmpty).min(1);
|
|
30748
|
+
var recorded = (shape, whatToRecord) => exports_external.object(shape, {
|
|
30749
|
+
error: (issue2) => issue2.input === undefined ? `missing \u2014 record ${whatToRecord}` : undefined
|
|
30750
|
+
}).passthrough();
|
|
30751
|
+
var gapAnalysisWriteSchema = recorded({
|
|
30752
|
+
findings: exports_external.array(exports_external.object({ finding: nonEmpty, answer: nonEmpty }).passthrough()).optional(),
|
|
30753
|
+
error: nonEmpty.optional()
|
|
30754
|
+
}, "the gap analyst's `findings`, each with how the plan answers it (`[]` when it found none), or its failed call's `error`").refine((analysis) => analysis.findings === undefined !== (analysis.error === undefined), {
|
|
30755
|
+
message: "record exactly one of `findings` or the failed call's `error`"
|
|
30756
|
+
});
|
|
30757
|
+
var planReviewWriteSchema = recorded({
|
|
30758
|
+
verdict: exports_external.enum(PLAN_REVIEW_VERDICTS),
|
|
30759
|
+
rounds: exports_external.number().int().min(1).max(PLAN_REVIEW_MAX_ROUNDS),
|
|
30760
|
+
remainingIssues: exports_external.array(exports_external.object({ issue: nonEmpty, evidence: nonEmpty }).passthrough()).optional(),
|
|
30761
|
+
error: nonEmpty.optional()
|
|
30762
|
+
}, "the plan review's `verdict` and `rounds`, with `remainingIssues` when it was rejected or `error` when a review's call failed").superRefine((review, ctx) => {
|
|
30763
|
+
const remaining = review.remainingIssues?.length ?? 0;
|
|
30764
|
+
if (review.verdict === "rejected" && remaining === 0) {
|
|
30765
|
+
ctx.addIssue({
|
|
30766
|
+
code: "custom",
|
|
30767
|
+
path: ["remainingIssues"],
|
|
30768
|
+
message: "a rejected review records the blocking issues its last round named"
|
|
30769
|
+
});
|
|
30770
|
+
}
|
|
30771
|
+
if (review.verdict === "rejected" && review.rounds < PLAN_REVIEW_MAX_ROUNDS) {
|
|
30772
|
+
ctx.addIssue({
|
|
30773
|
+
code: "custom",
|
|
30774
|
+
path: ["rounds"],
|
|
30775
|
+
message: `a review still rejecting after ${review.rounds} of ${PLAN_REVIEW_MAX_ROUNDS} rounds is revised and reviewed again, not recorded`
|
|
30776
|
+
});
|
|
30777
|
+
}
|
|
30778
|
+
if (review.verdict === "approved" && remaining > 0) {
|
|
30779
|
+
ctx.addIssue({
|
|
30780
|
+
code: "custom",
|
|
30781
|
+
path: ["remainingIssues"],
|
|
30782
|
+
message: "an approved review leaves no blocking issue standing"
|
|
30783
|
+
});
|
|
30784
|
+
}
|
|
30785
|
+
if (review.verdict === "failed" !== (review.error !== undefined)) {
|
|
30786
|
+
ctx.addIssue({
|
|
30787
|
+
code: "custom",
|
|
30788
|
+
path: ["error"],
|
|
30789
|
+
message: "a failed review records its call's error, and only a failed review does"
|
|
30790
|
+
});
|
|
30791
|
+
}
|
|
30792
|
+
});
|
|
30733
30793
|
var planWriteSchema = planSchema.extend({
|
|
30734
30794
|
requiredSkills: exports_external.object({
|
|
30735
30795
|
implement: nonEmptySkillList,
|
|
30736
30796
|
test: nonEmptySkillList,
|
|
30737
30797
|
review: nonEmptySkillList
|
|
30738
|
-
}).passthrough()
|
|
30798
|
+
}).passthrough(),
|
|
30799
|
+
gapAnalysis: gapAnalysisWriteSchema,
|
|
30800
|
+
planReview: planReviewWriteSchema
|
|
30739
30801
|
});
|
|
30740
30802
|
var phaseHandoffSchema = exports_external.discriminatedUnion("phase", [
|
|
30741
30803
|
architectSchema,
|
|
@@ -31423,6 +31485,9 @@ class DispatchClient {
|
|
|
31423
31485
|
async getMessageThread(id, session) {
|
|
31424
31486
|
return this.#json("GET", ["api", "v1", "messages", id], undefined, { session });
|
|
31425
31487
|
}
|
|
31488
|
+
async acceptMessageDelivery(id, attempt, input) {
|
|
31489
|
+
return this.#json("POST", ["api", "v1", "messages", id, "deliveries", String(attempt), "accept"], input);
|
|
31490
|
+
}
|
|
31426
31491
|
async artifact(issue2, input) {
|
|
31427
31492
|
const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"];
|
|
31428
31493
|
if ("content" in input)
|
|
@@ -33611,7 +33676,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
33611
33676
|
// package.json
|
|
33612
33677
|
var package_default = {
|
|
33613
33678
|
name: "@sjawhar/pi-legion-envoy",
|
|
33614
|
-
version: "5.
|
|
33679
|
+
version: "5.30.0",
|
|
33615
33680
|
type: "module",
|
|
33616
33681
|
omp: {
|
|
33617
33682
|
extensions: [
|
|
@@ -33656,6 +33721,101 @@ var package_default = {
|
|
|
33656
33721
|
}
|
|
33657
33722
|
};
|
|
33658
33723
|
|
|
33724
|
+
// src/dispatch-user-turn.ts
|
|
33725
|
+
function isUserTurnCandidate(rendered) {
|
|
33726
|
+
const { delivery } = rendered;
|
|
33727
|
+
return rendered.envelope?.source === "dispatch" && delivery?.resource === "message" && delivery.issueKey === null && delivery.broadcastId === undefined && rendered.dispatchActor?.kind === "user" && (delivery.mode === "aside" || delivery.mode === "steer");
|
|
33728
|
+
}
|
|
33729
|
+
var AcceptedDeliverySchema = exports_external.object({
|
|
33730
|
+
message_id: exports_external.string(),
|
|
33731
|
+
body: exports_external.string(),
|
|
33732
|
+
delivery: exports_external.enum(["aside", "steer"])
|
|
33733
|
+
});
|
|
33734
|
+
function turnFromAccept(accepted) {
|
|
33735
|
+
const parsed = AcceptedDeliverySchema.safeParse(accepted);
|
|
33736
|
+
if (!parsed.success)
|
|
33737
|
+
return;
|
|
33738
|
+
return { body: parsed.data.body, messageId: parsed.data.message_id, mode: parsed.data.delivery };
|
|
33739
|
+
}
|
|
33740
|
+
var HANDLED_ATTEMPT_ENTRY = "envoy-dispatch-handled-attempt";
|
|
33741
|
+
function handledAttemptKey(messageId, attempt) {
|
|
33742
|
+
return `${messageId}#${attempt}`;
|
|
33743
|
+
}
|
|
33744
|
+
function handledAttempts(entries) {
|
|
33745
|
+
const handled = new Set;
|
|
33746
|
+
for (const entry of entries) {
|
|
33747
|
+
if (typeof entry !== "object" || entry === null)
|
|
33748
|
+
continue;
|
|
33749
|
+
if (!("type" in entry) || entry.type !== "custom")
|
|
33750
|
+
continue;
|
|
33751
|
+
if (!("customType" in entry) || entry.customType !== HANDLED_ATTEMPT_ENTRY)
|
|
33752
|
+
continue;
|
|
33753
|
+
if (!("data" in entry) || typeof entry.data !== "object" || entry.data === null)
|
|
33754
|
+
continue;
|
|
33755
|
+
const { data } = entry;
|
|
33756
|
+
if (!("message_id" in data) || typeof data.message_id !== "string")
|
|
33757
|
+
continue;
|
|
33758
|
+
if (!("attempt" in data) || typeof data.attempt !== "number")
|
|
33759
|
+
continue;
|
|
33760
|
+
handled.add(handledAttemptKey(data.message_id, data.attempt));
|
|
33761
|
+
}
|
|
33762
|
+
return handled;
|
|
33763
|
+
}
|
|
33764
|
+
function userMessageText(content) {
|
|
33765
|
+
if (typeof content === "string")
|
|
33766
|
+
return content;
|
|
33767
|
+
if (!Array.isArray(content))
|
|
33768
|
+
return "";
|
|
33769
|
+
const pieces = [];
|
|
33770
|
+
for (const part of content) {
|
|
33771
|
+
if (typeof part !== "object" || part === null)
|
|
33772
|
+
continue;
|
|
33773
|
+
if ("type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
|
|
33774
|
+
pieces.push(part.text);
|
|
33775
|
+
}
|
|
33776
|
+
}
|
|
33777
|
+
return pieces.join(`
|
|
33778
|
+
`);
|
|
33779
|
+
}
|
|
33780
|
+
var INJECTED_USER_TURNS = Symbol.for("legion.pi-envoy.injected-user-turns");
|
|
33781
|
+
function injectedUserTurns() {
|
|
33782
|
+
const store = globalThis;
|
|
33783
|
+
const turns = store[INJECTED_USER_TURNS] ?? new Map;
|
|
33784
|
+
store[INJECTED_USER_TURNS] = turns;
|
|
33785
|
+
return turns;
|
|
33786
|
+
}
|
|
33787
|
+
function noteInjectedUserTurn(sessionID, body, messageId) {
|
|
33788
|
+
const turns = injectedUserTurns();
|
|
33789
|
+
const session = turns.get(sessionID) ?? { found: new Map, sent: [] };
|
|
33790
|
+
session.sent.push({ body, messageId });
|
|
33791
|
+
turns.set(sessionID, session);
|
|
33792
|
+
}
|
|
33793
|
+
function matchInjectedUserTurn(sessionID, message) {
|
|
33794
|
+
if (typeof message !== "object" || message === null)
|
|
33795
|
+
return;
|
|
33796
|
+
if (!("role" in message) || message.role !== "user")
|
|
33797
|
+
return;
|
|
33798
|
+
if (!("timestamp" in message) || typeof message.timestamp !== "number")
|
|
33799
|
+
return;
|
|
33800
|
+
const session = injectedUserTurns().get(sessionID);
|
|
33801
|
+
if (session === undefined)
|
|
33802
|
+
return;
|
|
33803
|
+
const known = session.found.get(message.timestamp);
|
|
33804
|
+
if (known !== undefined)
|
|
33805
|
+
return known;
|
|
33806
|
+
const text = userMessageText("content" in message ? message.content : undefined);
|
|
33807
|
+
const index = session.sent.findIndex((sent2) => sent2.body === text);
|
|
33808
|
+
const sent = session.sent[index];
|
|
33809
|
+
if (sent === undefined)
|
|
33810
|
+
return;
|
|
33811
|
+
session.sent.splice(index, 1);
|
|
33812
|
+
session.found.set(message.timestamp, sent.messageId);
|
|
33813
|
+
return sent.messageId;
|
|
33814
|
+
}
|
|
33815
|
+
function endInjectedUserTurns(sessionID) {
|
|
33816
|
+
injectedUserTurns().delete(sessionID);
|
|
33817
|
+
}
|
|
33818
|
+
|
|
33659
33819
|
// src/legion/classify.ts
|
|
33660
33820
|
function classifySession(env) {
|
|
33661
33821
|
if (env.LEGION_CONTROLLER !== undefined && env.LEGION_TREE !== undefined) {
|
|
@@ -35777,7 +35937,8 @@ function legionExtension(pi) {
|
|
|
35777
35937
|
pi.on("message_start", async (event, context) => {
|
|
35778
35938
|
if (!phaseWorkerSession(context))
|
|
35779
35939
|
return;
|
|
35780
|
-
const
|
|
35940
|
+
const injected = matchInjectedUserTurn(context.sessionManager.getSessionId(), event.message) !== undefined;
|
|
35941
|
+
const kind = injected ? "inbound-event" : inboundKind(event.message);
|
|
35781
35942
|
if (kind !== undefined)
|
|
35782
35943
|
advancePhaseStall({ kind });
|
|
35783
35944
|
});
|
|
@@ -6,7 +6,8 @@ human messages you directly from the Agents page.
|
|
|
6
6
|
## Targeted agent messages
|
|
7
7
|
|
|
8
8
|
A human — or any bearer caller over HTTP, such as a test rig — can target the issue message at a
|
|
9
|
-
live Envoy session or role as **BTW**, **Aside**, or **Steer
|
|
9
|
+
live Envoy session or role as **BTW**, **Aside**, or **Steer** (the dashboard calls a steer
|
|
10
|
+
**Send**). The incoming Dispatch frame names
|
|
10
11
|
the issue and includes a `reply_with` hint (`{ tool, args }`, ready to issue on any host); reply on the same open issue with the existing
|
|
11
12
|
tool, never a new targeted send:
|
|
12
13
|
|
|
@@ -32,9 +33,17 @@ target only a session that advertises the mode you want. Sending to a session wi
|
|
|
32
33
|
|
|
33
34
|
## Answering a direct message
|
|
34
35
|
|
|
35
|
-
A human can also message you directly from the **Agents** page, with no issue at all.
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
A human can also message you directly from the **Agents** page, with no issue at all. On Oh My
|
|
37
|
+
Pi, a person's **Send** or **Aside** arrives as their own user message, exactly as if they had
|
|
38
|
+
typed it at your terminal: your Envoy plugin takes it only once Dispatch accepts it as a person's
|
|
39
|
+
own fresh message to you, and injects the text Dispatch stored. Answer it in the conversation as
|
|
40
|
+
you would anything typed, with no `dispatch_message`; the Agents page shows your conversation
|
|
41
|
+
live, so they read your answer there.
|
|
42
|
+
|
|
43
|
+
Everything else still arrives as a Dispatch frame: a **BTW**, a broadcast, a direct message on a
|
|
44
|
+
host that takes no user turn from its plugin (Claude Code), and one Dispatch did not accept.
|
|
45
|
+
That frame names no issue and its `reply_with` hint carries none either; answer it with the
|
|
46
|
+
message's bare id in `in_reply_to`, alone:
|
|
38
47
|
|
|
39
48
|
```ts
|
|
40
49
|
dispatch_message({
|