@oh-my-pi/pi-coding-agent 16.3.8 → 16.3.10
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/CHANGELOG.md +37 -0
- package/dist/cli.js +3050 -3048
- package/dist/types/commands/say.d.ts +6 -1
- package/dist/types/commit/agentic/lock-files.d.ts +36 -0
- package/dist/types/config/model-discovery.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +5 -3
- package/dist/types/modes/session-observer-registry.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +11 -7
- package/dist/types/task/renderer.d.ts +0 -1
- package/dist/types/tools/browser/run-cancellation.d.ts +11 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +1 -0
- package/dist/types/tools/tool-errors.d.ts +1 -1
- package/package.json +12 -12
- package/src/commands/say.ts +73 -30
- package/src/commit/agentic/index.ts +3 -1
- package/src/commit/agentic/lock-files.ts +107 -0
- package/src/commit/agentic/tools/git-overview.ts +1 -20
- package/src/config/model-discovery.ts +6 -3
- package/src/config/model-registry.ts +18 -7
- package/src/discovery/claude.ts +12 -8
- package/src/extensibility/skills.ts +8 -5
- package/src/modes/components/__tests__/skill-message.test.ts +2 -0
- package/src/modes/components/agent-hub.ts +17 -2
- package/src/modes/components/skill-message.ts +1 -1
- package/src/modes/index.ts +0 -7
- package/src/modes/interactive-mode.ts +54 -18
- package/src/modes/session-observer-registry.ts +11 -8
- package/src/sdk.ts +18 -0
- package/src/session/agent-session.ts +25 -13
- package/src/task/render.test.ts +134 -1
- package/src/task/render.ts +24 -6
- package/src/task/renderer.ts +0 -1
- package/src/tools/browser/cmux/cmux-tab.ts +7 -3
- package/src/tools/browser/run-cancellation.ts +28 -8
- package/src/tools/browser/tab-protocol.ts +1 -1
- package/src/tools/browser/tab-supervisor.ts +4 -4
- package/src/tools/browser/tab-worker.ts +19 -7
- package/src/tools/irc.ts +18 -2
- package/src/tools/tool-errors.ts +3 -3
- package/src/tts/speakable.ts +19 -9
package/src/task/render.ts
CHANGED
|
@@ -1679,10 +1679,16 @@ function renderNestedTaskResults(
|
|
|
1679
1679
|
continue;
|
|
1680
1680
|
}
|
|
1681
1681
|
const ordered = orderResultsForDisplay(details.results);
|
|
1682
|
-
ordered
|
|
1683
|
-
|
|
1682
|
+
const visible = expanded ? ordered : selectCollapsedResults(ordered);
|
|
1683
|
+
const hiddenCount = ordered.length - visible.length;
|
|
1684
|
+
visible.forEach((result, index) => {
|
|
1685
|
+
const { prefix, continuePrefix } = nestedMarkers(hiddenCount === 0 && index === visible.length - 1, theme);
|
|
1684
1686
|
lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme, seen, depth + 1));
|
|
1685
1687
|
});
|
|
1688
|
+
if (hiddenCount > 0) {
|
|
1689
|
+
const { prefix } = nestedMarkers(true, theme);
|
|
1690
|
+
lines.push(`${prefix} ${theme.fg("dim", formatMoreItems(hiddenCount, "agent"))}`);
|
|
1691
|
+
}
|
|
1686
1692
|
seen.delete(details);
|
|
1687
1693
|
}
|
|
1688
1694
|
return lines;
|
|
@@ -1716,18 +1722,26 @@ function renderNestedTaskTree(
|
|
|
1716
1722
|
const hasResults = Boolean(details.results && details.results.length > 0);
|
|
1717
1723
|
if (hasResults) {
|
|
1718
1724
|
const ordered = orderResultsForDisplay(details.results);
|
|
1719
|
-
ordered
|
|
1720
|
-
|
|
1725
|
+
const visible = expanded ? ordered : selectCollapsedResults(ordered);
|
|
1726
|
+
const hiddenCount = ordered.length - visible.length;
|
|
1727
|
+
visible.forEach((result, index) => {
|
|
1728
|
+
const { prefix, continuePrefix } = nestedMarkers(hiddenCount === 0 && index === visible.length - 1, theme);
|
|
1721
1729
|
lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme, seen, depth + 1));
|
|
1722
1730
|
});
|
|
1731
|
+
if (hiddenCount > 0) {
|
|
1732
|
+
const { prefix } = nestedMarkers(true, theme);
|
|
1733
|
+
lines.push(`${prefix} ${theme.fg("dim", formatMoreItems(hiddenCount, "agent"))}`);
|
|
1734
|
+
}
|
|
1723
1735
|
seen.delete(details);
|
|
1724
1736
|
continue;
|
|
1725
1737
|
}
|
|
1726
1738
|
const inflight = details.progress;
|
|
1727
1739
|
if (inflight && inflight.length > 0) {
|
|
1728
1740
|
const ordered = orderProgressForDisplay(inflight);
|
|
1729
|
-
ordered.
|
|
1730
|
-
|
|
1741
|
+
const visible = expanded ? ordered : ordered.slice(Math.max(0, ordered.length - COLLAPSED_AGENT_LIMIT));
|
|
1742
|
+
const hiddenCount = ordered.length - visible.length;
|
|
1743
|
+
visible.forEach((prog, index) => {
|
|
1744
|
+
const { prefix, continuePrefix } = nestedMarkers(hiddenCount === 0 && index === visible.length - 1, theme);
|
|
1731
1745
|
lines.push(
|
|
1732
1746
|
...renderAgentProgress(
|
|
1733
1747
|
prog,
|
|
@@ -1742,6 +1756,10 @@ function renderNestedTaskTree(
|
|
|
1742
1756
|
),
|
|
1743
1757
|
);
|
|
1744
1758
|
});
|
|
1759
|
+
if (hiddenCount > 0) {
|
|
1760
|
+
const { prefix } = nestedMarkers(true, theme);
|
|
1761
|
+
lines.push(`${prefix} ${theme.fg("dim", formatMoreItems(hiddenCount, "agent"))}`);
|
|
1762
|
+
}
|
|
1745
1763
|
}
|
|
1746
1764
|
seen.delete(details);
|
|
1747
1765
|
}
|
package/src/task/renderer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { logger, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { logger, postmortem, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { JsRuntime, type RuntimeHooks } from "../../../eval/js/shared/runtime";
|
|
6
6
|
import type { JsDisplayOutput } from "../../../eval/js/shared/types";
|
|
7
7
|
import { callSessionTool } from "../../../eval/js/tool-bridge";
|
|
@@ -1306,7 +1306,11 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1306
1306
|
if (timeoutSignal.aborted) {
|
|
1307
1307
|
reject(new ToolError(`Browser code execution timed out after ${opts.timeoutMs}ms`));
|
|
1308
1308
|
} else {
|
|
1309
|
-
reject(
|
|
1309
|
+
reject(
|
|
1310
|
+
signal.reason instanceof ToolAbortError
|
|
1311
|
+
? signal.reason
|
|
1312
|
+
: new ToolAbortError(undefined, { cause: signal.reason }),
|
|
1313
|
+
);
|
|
1310
1314
|
}
|
|
1311
1315
|
};
|
|
1312
1316
|
if (signal.aborted) onAbort();
|
|
@@ -1336,7 +1340,7 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1336
1340
|
return { displays, returnValue: cloneSafe(returnValue), screenshots };
|
|
1337
1341
|
} finally {
|
|
1338
1342
|
signal.removeEventListener("abort", onAbort);
|
|
1339
|
-
runAc.abort(new ToolAbortError("Browser run ended"));
|
|
1343
|
+
runAc.abort(postmortem.markExpectedCleanupError(new ToolAbortError("Browser run ended")));
|
|
1340
1344
|
tab.clearRunContext();
|
|
1341
1345
|
}
|
|
1342
1346
|
}
|
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { throwIfAborted } from "../tool-errors";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Marks a run-scoped promise as observed without changing its behavior for awaited callers.
|
|
6
|
+
*
|
|
7
|
+
* Browser run teardown aborts can reject promises created for evaluated code after user code
|
|
8
|
+
* has stopped observing them (for example fire-and-forget `wait()`/facade calls). In 16.3.0
|
|
9
|
+
* those zero-consumer rejections reached the process-level `unhandledRejection` handler and
|
|
10
|
+
* killed every subagent sharing the process (issues #4499/#4672). Attaching a no-op rejection
|
|
11
|
+
* handler at creation makes the promise observed while returning the original promise so callers
|
|
12
|
+
* that do await it still receive the rejection.
|
|
13
|
+
*/
|
|
14
|
+
export function markHandled<T>(promise: Promise<T>): Promise<T> {
|
|
15
|
+
void promise.catch(() => undefined);
|
|
16
|
+
return promise;
|
|
17
|
+
}
|
|
18
|
+
|
|
4
19
|
/** Sleeps inside evaluated browser code while honoring the owning run's cancellation signal. */
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
20
|
+
export function waitForBrowserRun(ms: number, signal: AbortSignal): Promise<void> {
|
|
21
|
+
const promise = (async (): Promise<void> => {
|
|
22
|
+
throwIfAborted(signal);
|
|
23
|
+
await untilAborted(signal, () => Bun.sleep(ms));
|
|
24
|
+
throwIfAborted(signal);
|
|
25
|
+
})();
|
|
26
|
+
return markHandled(promise);
|
|
9
27
|
}
|
|
10
28
|
|
|
11
29
|
/** Binds a long-lived browser facade to one evaluated run's abort signal. */
|
|
@@ -24,10 +42,12 @@ export function bindBrowserRunFacade<T extends object>(target: T, signal: AbortS
|
|
|
24
42
|
if (result && typeof result === "object") {
|
|
25
43
|
const then = Reflect.get(result, "then");
|
|
26
44
|
if (typeof then === "function") {
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
return markHandled(
|
|
46
|
+
Promise.resolve(result).then(resolved => {
|
|
47
|
+
throwIfAborted(signal);
|
|
48
|
+
return resolved;
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
31
51
|
}
|
|
32
52
|
}
|
|
33
53
|
throwIfAborted(signal);
|
|
@@ -66,7 +66,7 @@ export type ToolReply = { ok: true; value: unknown } | { ok: false; error: RunEr
|
|
|
66
66
|
export type WorkerInbound =
|
|
67
67
|
| { type: "init"; payload: WorkerInitPayload }
|
|
68
68
|
| { type: "run"; id: string; name: string; code: string; timeoutMs: number; session: SessionSnapshot }
|
|
69
|
-
| { type: "abort"; id: string }
|
|
69
|
+
| { type: "abort"; id: string; expectedCleanup?: boolean }
|
|
70
70
|
| { type: "tool-reply"; id: string; reply: ToolReply }
|
|
71
71
|
| { type: "close" };
|
|
72
72
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getPuppeteerDir, logger, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { getPuppeteerDir, logger, postmortem, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { Page, Target } from "puppeteer-core";
|
|
3
3
|
import { callSessionTool } from "../../eval/js/tool-bridge";
|
|
4
4
|
import { webpExclusionForModel } from "../../utils/image-loading";
|
|
@@ -486,11 +486,11 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
486
486
|
}
|
|
487
487
|
const wasAlive = tab.state === "alive";
|
|
488
488
|
tab.state = "dead";
|
|
489
|
-
const closeError = new ToolError(`Tab ${JSON.stringify(name)} was closed`);
|
|
489
|
+
const closeError = postmortem.markExpectedCleanupError(new ToolError(`Tab ${JSON.stringify(name)} was closed`));
|
|
490
490
|
for (const [id, pending] of tab.pending) {
|
|
491
491
|
if (tab.backend === "worker") {
|
|
492
492
|
try {
|
|
493
|
-
tab.worker.send({ type: "abort", id });
|
|
493
|
+
tab.worker.send({ type: "abort", id, expectedCleanup: true });
|
|
494
494
|
} catch {}
|
|
495
495
|
}
|
|
496
496
|
for (const ctrl of pending.toolCalls.values()) ctrl.abort(closeError);
|
|
@@ -744,7 +744,7 @@ async function forceKillTab(name: string, reason: string): Promise<void> {
|
|
|
744
744
|
const tab = tabs.get(name);
|
|
745
745
|
if (!tab) return;
|
|
746
746
|
tab.state = "dead";
|
|
747
|
-
const error = new ToolError(reason);
|
|
747
|
+
const error = postmortem.markExpectedCleanupError(new ToolError(reason));
|
|
748
748
|
for (const pending of tab.pending.values()) pending.reject(error);
|
|
749
749
|
tab.pending.clear();
|
|
750
750
|
if (tab.backend === "cmux") {
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { postmortem, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import type { HTMLElement } from "linkedom";
|
|
7
7
|
import type {
|
|
8
8
|
Browser,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
loadPuppeteerInWorker,
|
|
37
37
|
} from "./launch";
|
|
38
38
|
import { extractReadableFromHtml, type ReadableFormat } from "./readable";
|
|
39
|
-
import { waitForBrowserRun } from "./run-cancellation";
|
|
39
|
+
import { markHandled, waitForBrowserRun } from "./run-cancellation";
|
|
40
40
|
import type {
|
|
41
41
|
Observation,
|
|
42
42
|
ObservationEntry,
|
|
@@ -593,7 +593,12 @@ export class WorkerCore {
|
|
|
593
593
|
await this.#run(msg);
|
|
594
594
|
return;
|
|
595
595
|
case "abort":
|
|
596
|
-
if (this.#active?.id === msg.id)
|
|
596
|
+
if (this.#active?.id === msg.id) {
|
|
597
|
+
const reason = msg.expectedCleanup
|
|
598
|
+
? postmortem.markExpectedCleanupError(new ToolAbortError())
|
|
599
|
+
: new ToolAbortError();
|
|
600
|
+
this.#active.ac.abort(reason);
|
|
601
|
+
}
|
|
597
602
|
return;
|
|
598
603
|
case "tool-reply":
|
|
599
604
|
this.#deliverToolReply(msg.id, msg.reply);
|
|
@@ -732,6 +737,10 @@ export class WorkerCore {
|
|
|
732
737
|
});
|
|
733
738
|
const { promise: cancelRejection, reject: rejectCancel } = Promise.withResolvers<never>();
|
|
734
739
|
const onCancel = (): void => {
|
|
740
|
+
const abortError =
|
|
741
|
+
signal.reason instanceof ToolAbortError
|
|
742
|
+
? signal.reason
|
|
743
|
+
: new ToolAbortError(undefined, { cause: signal.reason });
|
|
735
744
|
if (timeoutSignal.aborted) {
|
|
736
745
|
const stalled = describeInflight(active.inflight);
|
|
737
746
|
rejectCancel(
|
|
@@ -740,11 +749,14 @@ export class WorkerCore {
|
|
|
740
749
|
),
|
|
741
750
|
);
|
|
742
751
|
} else {
|
|
743
|
-
rejectCancel(
|
|
752
|
+
rejectCancel(abortError);
|
|
744
753
|
}
|
|
745
754
|
// Cancel in-flight tool calls so user code's awaited proxies reject promptly.
|
|
755
|
+
const toolAbort = timeoutSignal.aborted
|
|
756
|
+
? postmortem.markExpectedCleanupError(new ToolAbortError(undefined, { cause: timeoutSignal.reason }))
|
|
757
|
+
: abortError;
|
|
746
758
|
for (const pending of active.pendingTools.values()) {
|
|
747
|
-
pending.reject(
|
|
759
|
+
pending.reject(toolAbort);
|
|
748
760
|
}
|
|
749
761
|
active.pendingTools.clear();
|
|
750
762
|
};
|
|
@@ -771,7 +783,7 @@ export class WorkerCore {
|
|
|
771
783
|
this.#transport.send({ type: "result", id: msg.id, ok: false, error: errorPayload(error) });
|
|
772
784
|
} finally {
|
|
773
785
|
if (this.#active?.id === msg.id) this.#active = null;
|
|
774
|
-
runAc.abort(new ToolAbortError("Browser run ended"));
|
|
786
|
+
runAc.abort(postmortem.markExpectedCleanupError(new ToolAbortError("Browser run ended")));
|
|
775
787
|
}
|
|
776
788
|
}
|
|
777
789
|
|
|
@@ -889,7 +901,7 @@ export class WorkerCore {
|
|
|
889
901
|
const waitMs = (explicit?: number): number => resolveWaitTimeout(timeoutMs, explicit);
|
|
890
902
|
const INF = Number.POSITIVE_INFINITY;
|
|
891
903
|
const op = <T>(label: string, perOpMs: number, fn: (sig: AbortSignal) => Promise<T>): Promise<T> =>
|
|
892
|
-
this.#runOp(active, label, signal, perOpMs, fn);
|
|
904
|
+
markHandled(this.#runOp(active, label, signal, perOpMs, fn));
|
|
893
905
|
return {
|
|
894
906
|
name,
|
|
895
907
|
page,
|
package/src/tools/irc.ts
CHANGED
|
@@ -171,7 +171,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
171
171
|
case "send":
|
|
172
172
|
return this.#executeSend(registry, senderId, params, signal);
|
|
173
173
|
case "wait":
|
|
174
|
-
return this.#executeWait(senderId, params, signal);
|
|
174
|
+
return this.#executeWait(registry, senderId, params, signal);
|
|
175
175
|
case "inbox":
|
|
176
176
|
return this.#executeInbox(registry, senderId, params);
|
|
177
177
|
default:
|
|
@@ -367,8 +367,24 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
async #executeWait(
|
|
370
|
+
async #executeWait(
|
|
371
|
+
registry: AgentRegistry,
|
|
372
|
+
senderId: string,
|
|
373
|
+
params: IrcParams,
|
|
374
|
+
signal?: AbortSignal,
|
|
375
|
+
): Promise<AgentToolResult<IrcDetails>> {
|
|
371
376
|
const from = params.from?.trim() || undefined;
|
|
377
|
+
const session = registry.get(senderId)?.session;
|
|
378
|
+
const pending =
|
|
379
|
+
typeof session?.drainPendingIrcInboxMessages === "function"
|
|
380
|
+
? session.drainPendingIrcInboxMessages(senderId, { from, limit: 1 })[0]
|
|
381
|
+
: undefined;
|
|
382
|
+
if (pending) {
|
|
383
|
+
return {
|
|
384
|
+
content: [{ type: "text", text: formatIncoming(pending) }],
|
|
385
|
+
details: { op: "wait", from: senderId, waited: pending },
|
|
386
|
+
};
|
|
387
|
+
}
|
|
372
388
|
const timeoutMs = this.#resolveTimeoutMs(params);
|
|
373
389
|
const waited = await IrcBus.global().wait(senderId, { from }, timeoutMs, signal);
|
|
374
390
|
if (!waited) {
|
package/src/tools/tool-errors.ts
CHANGED
|
@@ -30,8 +30,8 @@ export class ToolError extends Error {
|
|
|
30
30
|
export class ToolAbortError extends Error {
|
|
31
31
|
static readonly MESSAGE = "Operation aborted";
|
|
32
32
|
|
|
33
|
-
constructor(message: string = ToolAbortError.MESSAGE) {
|
|
34
|
-
super(message);
|
|
33
|
+
constructor(message: string = ToolAbortError.MESSAGE, options?: ErrorOptions) {
|
|
34
|
+
super(message, options);
|
|
35
35
|
this.name = "ToolAbortError";
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -43,7 +43,7 @@ export class ToolAbortError extends Error {
|
|
|
43
43
|
export function throwIfAborted(signal?: AbortSignal): void {
|
|
44
44
|
if (signal?.aborted) {
|
|
45
45
|
const reason = signal.reason instanceof Error ? signal.reason : undefined;
|
|
46
|
-
throw reason instanceof ToolAbortError ? reason : new ToolAbortError();
|
|
46
|
+
throw reason instanceof ToolAbortError ? reason : new ToolAbortError(undefined, { cause: signal.reason });
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
package/src/tts/speakable.ts
CHANGED
|
@@ -318,15 +318,20 @@ export class SpeakableStream {
|
|
|
318
318
|
this.#drain(out);
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
/**
|
|
321
|
+
/**
|
|
322
|
+
* Emit every buffered character. Runs the bounded streaming segmenter first
|
|
323
|
+
* so a large buffer (paste-sized delta, one-shot push) prefers sentence and
|
|
324
|
+
* clause cuts within {@link MAX_SEGMENT}, instead of word-splitting whole
|
|
325
|
+
* paragraphs at the cap ("…a big jump is" / "coming"). Not byte-identical to
|
|
326
|
+
* char-by-char streaming — the soft-clause latency cut can fire earlier
|
|
327
|
+
* there — but every segment obeys the same cap and boundary preferences.
|
|
328
|
+
* {@link #extract} leaves at most MAX_SEGMENT behind, emitted as the
|
|
329
|
+
* trailing segment.
|
|
330
|
+
*/
|
|
322
331
|
#drain(out: string[]): void {
|
|
323
|
-
|
|
332
|
+
this.#extract(out);
|
|
333
|
+
const text = this.#buf;
|
|
324
334
|
this.#buf = "";
|
|
325
|
-
while (text.length > MAX_SEGMENT) {
|
|
326
|
-
const cut = findForcedCut(text, MAX_SEGMENT);
|
|
327
|
-
this.#emit(text.slice(0, cut), out);
|
|
328
|
-
text = text.slice(cut);
|
|
329
|
-
}
|
|
330
335
|
this.#emit(text, out);
|
|
331
336
|
}
|
|
332
337
|
|
|
@@ -335,14 +340,19 @@ export class SpeakableStream {
|
|
|
335
340
|
for (;;) {
|
|
336
341
|
const buf = this.#buf;
|
|
337
342
|
const min = this.#spoke ? MIN_SEGMENT : FIRST_SEGMENT_MIN;
|
|
343
|
+
// Bounded: a sentence past MAX_SEGMENT risks Kokoro's ~510-phoneme
|
|
344
|
+
// truncation — fall through to clause/word cuts instead.
|
|
338
345
|
const sentence = findSentenceCut(buf, min);
|
|
339
|
-
if (sentence !== -1) {
|
|
346
|
+
if (sentence !== -1 && sentence <= MAX_SEGMENT) {
|
|
340
347
|
this.#cut(sentence, out);
|
|
341
348
|
continue;
|
|
342
349
|
}
|
|
343
350
|
if (!this.#spoke && buf.length >= FIRST_CLAUSE_MIN) {
|
|
351
|
+
// Bounded like the sentence branch: in a one-shot buffer the earliest
|
|
352
|
+
// clause can lie far past the cap; per-char streaming would have
|
|
353
|
+
// force-cut at FIRST_FORCED_MAX long before seeing it.
|
|
344
354
|
const clause = findClauseCut(buf, FIRST_SEGMENT_MIN);
|
|
345
|
-
if (clause !== -1) {
|
|
355
|
+
if (clause !== -1 && clause <= FIRST_FORCED_MAX) {
|
|
346
356
|
this.#cut(clause, out);
|
|
347
357
|
continue;
|
|
348
358
|
}
|