@dorokuma/herdsman-pi 0.13.3 → 0.13.5
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/package.json +1 -1
- package/src/turn-signal.ts +32 -9
- package/src/wake.ts +14 -1
package/package.json
CHANGED
package/src/turn-signal.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const TURN_SIGNAL_TIMEOUT_MS = 3_000;
|
|
|
4
4
|
export const TURN_SIGNAL_POLL_MS = 50;
|
|
5
5
|
export const TURN_SIGNAL_TAIL_CHARS = 8_000;
|
|
6
6
|
|
|
7
|
-
export type TurnCompletionCheckReason = "already_written" | "
|
|
7
|
+
export type TurnCompletionCheckReason = "already_written" | "timeout" | "unavailable";
|
|
8
8
|
|
|
9
9
|
export type TurnCompletionCheck = {
|
|
10
10
|
confirmed: boolean;
|
|
@@ -56,11 +56,30 @@ function defaultProbe(): SessionWriteProbe {
|
|
|
56
56
|
/**
|
|
57
57
|
* Bounded confirmation that the final assistant message reached the Pi session
|
|
58
58
|
* file before the extension signals the daemon. The message may already be on
|
|
59
|
-
* disk when the `message_end` hook fires
|
|
60
|
-
* the expected text or its
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
59
|
+
* disk when the `message_end` hook fires; otherwise the file tail is polled
|
|
60
|
+
* until the expected text (or its JSONL-escaped form) appears.
|
|
61
|
+
*
|
|
62
|
+
* The root cause of the mismatches this guards against is the source of
|
|
63
|
+
* `expectedText`, not the tail it is compared to: the extension reports the
|
|
64
|
+
* *sanitized* text it saw at `message_end` (`assistantMessageText` in
|
|
65
|
+
* `index.ts` runs the body through `sanitizeText`), while rewrite-type
|
|
66
|
+
* extensions (e.g. `no-tables` turning `### X` into bold or tables into
|
|
67
|
+
* bullets, plus link handling) keep rewriting the transcript that lands on disk
|
|
68
|
+
* *after* that hook. The client-side text and the on-disk original are therefore
|
|
69
|
+
* systematically inconsistent, and the disk original is the authoritative body —
|
|
70
|
+
* never the reported guess. "The file grew" was only a secondary mechanism that
|
|
71
|
+
* let such a mismatch be *confirmed* instead of caught (tool results, streamed
|
|
72
|
+
* partials and any other session write also grow the file): that is how
|
|
73
|
+
* mismatch signals ended up as empty `agent.done` releases (sample figures and
|
|
74
|
+
* the caveats around them live in
|
|
75
|
+
* `.agents/notes/20261002-empty-wake-expectedtext-rootcause.md`). The
|
|
76
|
+
* daemon's `expected_text_mismatch` arm now trusts the on-disk body, so a
|
|
77
|
+
* timeout here degrades honestly instead of blanking it. Do not trace this back
|
|
78
|
+
* to tail guessing alone.
|
|
79
|
+
*
|
|
80
|
+
* A timeout (or an unavailable file) still resolves so the caller can signal
|
|
81
|
+
* with the actual status instead of hanging forever, and it now reports
|
|
82
|
+
* `confirmed: false` honestly instead of fabricating a confirmation.
|
|
64
83
|
*/
|
|
65
84
|
export async function confirmSessionWrite(input: {
|
|
66
85
|
expectedText: string;
|
|
@@ -75,8 +94,14 @@ export async function confirmSessionWrite(input: {
|
|
|
75
94
|
const expectedText = input.expectedText.trim();
|
|
76
95
|
const deadline = Date.now() + timeoutMs;
|
|
77
96
|
const candidate = expectedText.length > 200 ? expectedText.slice(-200) : expectedText;
|
|
97
|
+
// The session file is JSONL, so the written text is escaped (`\n`, `\"`,
|
|
98
|
+
// `\\`). Matching the raw candidate only would miss every multi-line final
|
|
99
|
+
// message, so the escaped form of the same suffix is accepted as well.
|
|
100
|
+
const escapedCandidate = JSON.stringify(candidate).slice(1, -1);
|
|
78
101
|
const containsText = (tail: string | null) =>
|
|
79
|
-
tail !== null &&
|
|
102
|
+
tail !== null &&
|
|
103
|
+
candidate.length > 0 &&
|
|
104
|
+
(tail.includes(candidate) || tail.includes(escapedCandidate));
|
|
80
105
|
|
|
81
106
|
const initialSize = probe.size(input.path);
|
|
82
107
|
if (initialSize === null) return { confirmed: false, reason: "unavailable" };
|
|
@@ -85,8 +110,6 @@ export async function confirmSessionWrite(input: {
|
|
|
85
110
|
}
|
|
86
111
|
while (Date.now() < deadline) {
|
|
87
112
|
await probe.sleep(pollMs);
|
|
88
|
-
const size = probe.size(input.path);
|
|
89
|
-
if (size !== null && size > initialSize) return { confirmed: true, reason: "new_content" };
|
|
90
113
|
if (containsText(probe.readTail(input.path, TURN_SIGNAL_TAIL_CHARS))) {
|
|
91
114
|
return { confirmed: true, reason: "already_written" };
|
|
92
115
|
}
|
package/src/wake.ts
CHANGED
|
@@ -117,7 +117,20 @@ export function formatAgentOutcomeUpdates(outcomes: AgentOutcome[]): string {
|
|
|
117
117
|
const pane = outcome.paneId ?? "unknown";
|
|
118
118
|
if (outcome.kind === "failed") {
|
|
119
119
|
const reason = outcome.reason && outcome.reason.length > 0 ? outcome.reason : "(unknown)";
|
|
120
|
-
|
|
120
|
+
// A failed round can still have produced assistant text before it failed; a
|
|
121
|
+
// reason-only line left that body invisible to the orchestrator. But the
|
|
122
|
+
// body of a `failed` / `discarded` outcome is not this round's output:
|
|
123
|
+
// `#appendPlanFailedEvent` / `#appendPlanDiscardedEvent` pass the plan
|
|
124
|
+
// baseline snapshot (`plan.compactHistory`, written once at plan creation),
|
|
125
|
+
// which may hold an earlier round's answer (see the plan-baseline notes).
|
|
126
|
+
// Rendering it as `last assistant:` read as "this round produced this", so
|
|
127
|
+
// the label says pre-round. The wire record carries no assistant `ref`, so
|
|
128
|
+
// same-source cannot be decided here without widening the daemon payload;
|
|
129
|
+
// the label is therefore always explicit (and only dropped when the body is
|
|
130
|
+
// empty, keeping the reason-only line byte-for-byte unchanged).
|
|
131
|
+
const body =
|
|
132
|
+
outcome.text.length > 0 ? `\n last assistant (pre-round): ${outcome.text}` : "";
|
|
133
|
+
return `- failed ${identity} ${pane}\n reason: ${reason}${body}`;
|
|
121
134
|
}
|
|
122
135
|
const excerpt = outcome.text.length > 0 ? outcome.text : "(no assistant message)";
|
|
123
136
|
return `- ${outcome.kind} ${identity} ${pane}\n last assistant: ${excerpt}\n event: ${outcome.eventId}`;
|