@oh-my-pi/pi-coding-agent 17.0.3 → 17.0.4
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 +10 -0
- package/dist/cli.js +2355 -2355
- package/dist/types/mnemopi/state.d.ts +32 -9
- package/dist/types/task/types.d.ts +6 -6
- package/package.json +12 -12
- package/src/mnemopi/backend.ts +1 -1
- package/src/mnemopi/embed-worker.ts +8 -7
- package/src/mnemopi/state.ts +45 -18
- package/src/modes/components/transcript-container.ts +13 -7
- package/src/session/session-loader.ts +31 -3
- package/src/stt/recorder.ts +68 -55
- package/src/task/types.ts +11 -8
|
@@ -116,11 +116,15 @@ export declare class MnemopiSessionState {
|
|
|
116
116
|
beforeAgentStartPrompt(promptText: string): Promise<string | undefined>;
|
|
117
117
|
recallForCompaction(messages: AgentMessage[]): Promise<string | undefined>;
|
|
118
118
|
maybeRetainOnAgentEnd(_messages: AgentMessage[]): Promise<void>;
|
|
119
|
-
forceRetainCurrentSession(
|
|
119
|
+
forceRetainCurrentSession(options?: {
|
|
120
|
+
extract?: boolean;
|
|
121
|
+
}): Promise<void>;
|
|
120
122
|
retainMessages(messages: Array<{
|
|
121
123
|
role: string;
|
|
122
124
|
content: string;
|
|
123
|
-
}>, sourceId: string
|
|
125
|
+
}>, sourceId: string, options?: {
|
|
126
|
+
extract?: boolean;
|
|
127
|
+
}): Promise<void>;
|
|
124
128
|
attachSessionListeners(): void;
|
|
125
129
|
maybeRecallOnAgentStart(): Promise<void>;
|
|
126
130
|
/**
|
|
@@ -139,15 +143,34 @@ export declare class MnemopiSessionState {
|
|
|
139
143
|
* otherwise enqueue would report success while leaving the subagent's
|
|
140
144
|
* retained memories unconsolidated until the parent eventually shuts down
|
|
141
145
|
* (PR #2327 review).
|
|
146
|
+
*
|
|
147
|
+
* @param options.full - When true, run `sleepAllSessions` on every owned bank
|
|
148
|
+
* (the full cross-session consolidation used by `/memory enqueue`). When
|
|
149
|
+
* false (the default), run only `sleep` on the current session for a
|
|
150
|
+
* lighter, bounded shutdown pass.
|
|
151
|
+
* @param options.sleep - When false, skips the bank sleep step entirely.
|
|
152
|
+
* Used on the interactive shutdown path so `dispose` does not block on
|
|
153
|
+
* synchronous consolidation of old working rows from previous sessions.
|
|
154
|
+
* @param options.extract - When false, the retained transcript is stored but
|
|
155
|
+
* no LLM fact extraction is scheduled. Used on the interactive shutdown path
|
|
156
|
+
* so `dispose` does not block on a fresh LLM round-trip.
|
|
142
157
|
*/
|
|
143
|
-
consolidate(
|
|
158
|
+
consolidate(options?: {
|
|
159
|
+
full?: boolean;
|
|
160
|
+
extract?: boolean;
|
|
161
|
+
sleep?: boolean;
|
|
162
|
+
}): Promise<void>;
|
|
144
163
|
/**
|
|
145
|
-
* Release the per-session resources. Defaults to running
|
|
146
|
-
* before closing handles
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
164
|
+
* Release the per-session resources. Defaults to running a lighter
|
|
165
|
+
* {@link consolidate} pass before closing handles: it retains the current
|
|
166
|
+
* transcript and flushes in-flight extractions, but skips the synchronous
|
|
167
|
+
* bank sleep so normal session shutdown returns promptly. Full promotion of
|
|
168
|
+
* working memory into long-term storage is still performed by the explicit
|
|
169
|
+
* `/memory enqueue` and backend enqueue paths. Callers that are about to
|
|
170
|
+
* delete the DB files — e.g. `mnemopiBackend.clear` — pass
|
|
171
|
+
* `{ consolidate: false }` to skip the retain/flush pass, since spending
|
|
172
|
+
* tokens on memories that will be wiped on the next line is wasted work
|
|
173
|
+
* (PR #2327 review).
|
|
151
174
|
*
|
|
152
175
|
* `timeoutMs` caps how long the consolidate await blocks the caller
|
|
153
176
|
* (the user-visible `/quit` / `/exit` shutdown path passes this so
|
|
@@ -83,7 +83,7 @@ export declare const taskItemSchema: import("arktype/internal/variants/object.ts
|
|
|
83
83
|
name?: string | undefined;
|
|
84
84
|
agent: import("arktype/internal/attributes.ts").Default<string, "task">;
|
|
85
85
|
task: string;
|
|
86
|
-
outputSchema?:
|
|
86
|
+
outputSchema?: string | boolean | object | null | undefined;
|
|
87
87
|
schemaMode?: "permissive" | "strict" | undefined;
|
|
88
88
|
}, {}>;
|
|
89
89
|
/** Single task item. Fields are optional defensively: args stream in token by token. */
|
|
@@ -105,7 +105,7 @@ export declare const taskSchema: import("arktype/internal/variants/object.ts").O
|
|
|
105
105
|
name?: string | undefined;
|
|
106
106
|
agent: import("arktype/internal/attributes.ts").Default<string, "task">;
|
|
107
107
|
task: string;
|
|
108
|
-
outputSchema?:
|
|
108
|
+
outputSchema?: string | boolean | object | null | undefined;
|
|
109
109
|
schemaMode?: "permissive" | "strict" | undefined;
|
|
110
110
|
isolated?: boolean | undefined;
|
|
111
111
|
}, {}>;
|
|
@@ -113,14 +113,14 @@ declare const ALL_TASK_SCHEMAS: readonly [import("arktype/internal/variants/obje
|
|
|
113
113
|
name?: string | undefined;
|
|
114
114
|
agent: import("arktype/internal/attributes.ts").Default<string, "task">;
|
|
115
115
|
task: string;
|
|
116
|
-
outputSchema?:
|
|
116
|
+
outputSchema?: string | boolean | object | null | undefined;
|
|
117
117
|
schemaMode?: "permissive" | "strict" | undefined;
|
|
118
118
|
isolated?: boolean | undefined;
|
|
119
119
|
}, {}>, import("arktype/internal/variants/object.ts").ObjectType<{
|
|
120
120
|
name?: string | undefined;
|
|
121
121
|
agent: import("arktype/internal/attributes.ts").Default<string, "task">;
|
|
122
122
|
task: string;
|
|
123
|
-
outputSchema?:
|
|
123
|
+
outputSchema?: string | boolean | object | null | undefined;
|
|
124
124
|
schemaMode?: "permissive" | "strict" | undefined;
|
|
125
125
|
}, {}>, import("arktype/internal/variants/object.ts").ObjectType<{
|
|
126
126
|
context: string;
|
|
@@ -128,7 +128,7 @@ declare const ALL_TASK_SCHEMAS: readonly [import("arktype/internal/variants/obje
|
|
|
128
128
|
name?: string | undefined;
|
|
129
129
|
agent: import("arktype/internal/attributes.ts").Default<string, "task">;
|
|
130
130
|
task: string;
|
|
131
|
-
outputSchema?:
|
|
131
|
+
outputSchema?: string | boolean | object | null | undefined;
|
|
132
132
|
schemaMode?: "permissive" | "strict" | undefined;
|
|
133
133
|
isolated?: boolean | undefined;
|
|
134
134
|
}[];
|
|
@@ -138,7 +138,7 @@ declare const ALL_TASK_SCHEMAS: readonly [import("arktype/internal/variants/obje
|
|
|
138
138
|
name?: string | undefined;
|
|
139
139
|
agent: import("arktype/internal/attributes.ts").Default<string, "task">;
|
|
140
140
|
task: string;
|
|
141
|
-
outputSchema?:
|
|
141
|
+
outputSchema?: string | boolean | object | null | undefined;
|
|
142
142
|
schemaMode?: "permissive" | "strict" | undefined;
|
|
143
143
|
}[];
|
|
144
144
|
}, {}>];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "17.0.
|
|
4
|
+
"version": "17.0.4",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
53
53
|
"@babel/parser": "^7.29.7",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@oh-my-pi/hashline": "17.0.
|
|
56
|
-
"@oh-my-pi/omp-stats": "17.0.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "17.0.
|
|
58
|
-
"@oh-my-pi/pi-ai": "17.0.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "17.0.
|
|
61
|
-
"@oh-my-pi/pi-natives": "17.0.
|
|
62
|
-
"@oh-my-pi/pi-tui": "17.0.
|
|
63
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
64
|
-
"@oh-my-pi/pi-wire": "17.0.
|
|
65
|
-
"@oh-my-pi/snapcompact": "17.0.
|
|
55
|
+
"@oh-my-pi/hashline": "17.0.4",
|
|
56
|
+
"@oh-my-pi/omp-stats": "17.0.4",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "17.0.4",
|
|
58
|
+
"@oh-my-pi/pi-ai": "17.0.4",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "17.0.4",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "17.0.4",
|
|
61
|
+
"@oh-my-pi/pi-natives": "17.0.4",
|
|
62
|
+
"@oh-my-pi/pi-tui": "17.0.4",
|
|
63
|
+
"@oh-my-pi/pi-utils": "17.0.4",
|
|
64
|
+
"@oh-my-pi/pi-wire": "17.0.4",
|
|
65
|
+
"@oh-my-pi/snapcompact": "17.0.4",
|
|
66
66
|
"@opentelemetry/api": "^1.9.1",
|
|
67
67
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
68
68
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
package/src/mnemopi/backend.ts
CHANGED
|
@@ -145,7 +145,7 @@ export const mnemopiBackend: MemoryBackend = {
|
|
|
145
145
|
state = new MnemopiSessionState({ sessionId: session.sessionId, config, session });
|
|
146
146
|
setMnemopiSessionState(session, state);
|
|
147
147
|
}
|
|
148
|
-
await state?.consolidate();
|
|
148
|
+
await state?.consolidate({ full: true });
|
|
149
149
|
} catch (error) {
|
|
150
150
|
logger.warn("Mnemopi: enqueue failed.", { error: String(error) });
|
|
151
151
|
}
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
* in either process.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type
|
|
13
|
-
import { loadFastembed } from "@oh-my-pi/pi-mnemopi/core/fastembed-runtime";
|
|
12
|
+
import { defaultLocalModelInitializer, type StandardEmbeddingModel } from "@oh-my-pi/pi-mnemopi/core";
|
|
14
13
|
import type { MnemopiEmbedModelId, MnemopiEmbedTransport, MnemopiEmbedWorkerInbound } from "./embed-protocol";
|
|
15
14
|
|
|
16
15
|
interface LoadedModel {
|
|
@@ -25,12 +24,14 @@ let loaded: Promise<LoadedModel> | null = null;
|
|
|
25
24
|
let loadedKey = "";
|
|
26
25
|
|
|
27
26
|
async function loadModel(model: MnemopiEmbedModelId, cacheDir: string | undefined): Promise<LoadedModel> {
|
|
28
|
-
|
|
27
|
+
// Route through mnemopi's shared initializer so the worker inherits BOTH
|
|
28
|
+
// cache heals (sidecar re-fetch AND corrupt-model quarantine/retry) —
|
|
29
|
+
// fastembed/onnxruntime still load only in this child address space, the
|
|
30
|
+
// initializer calls loadFastembed() itself.
|
|
29
31
|
// Cast: `model` arrives as a string from the parent (resolved by
|
|
30
|
-
// mnemopi's `fastembedModelName`)
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
const instance = await FlagEmbedding.init({
|
|
32
|
+
// mnemopi's `fastembedModelName`); the parent only ever passes pre-vetted
|
|
33
|
+
// fast-* identifiers.
|
|
34
|
+
const instance = await defaultLocalModelInitializer({
|
|
34
35
|
model: model as StandardEmbeddingModel,
|
|
35
36
|
cacheDir,
|
|
36
37
|
showDownloadProgress: false,
|
package/src/mnemopi/state.ts
CHANGED
|
@@ -454,18 +454,23 @@ export class MnemopiSessionState {
|
|
|
454
454
|
this.lastRetainedTurn = userTurns;
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
async forceRetainCurrentSession(): Promise<void> {
|
|
457
|
+
async forceRetainCurrentSession(options: { extract?: boolean } = {}): Promise<void> {
|
|
458
458
|
if (this.aliasOf) return;
|
|
459
459
|
const flat = extractMessages(this.session.sessionManager);
|
|
460
|
-
await this.retainMessages(flat, this.sessionId);
|
|
460
|
+
await this.retainMessages(flat, this.sessionId, options);
|
|
461
461
|
this.lastRetainedTurn = flat.filter(message => message.role === "user").length;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
-
async retainMessages(
|
|
464
|
+
async retainMessages(
|
|
465
|
+
messages: Array<{ role: string; content: string }>,
|
|
466
|
+
sourceId: string,
|
|
467
|
+
options: { extract?: boolean } = {},
|
|
468
|
+
): Promise<void> {
|
|
465
469
|
const { transcript, messageCount } = prepareRetentionTranscript(messages, true);
|
|
466
470
|
if (!transcript) return;
|
|
467
471
|
const { transcript: extractText } = prepareUserRetentionTranscript(messages);
|
|
468
472
|
const { transcript: embedText } = prepareEmbeddableRetentionTranscript(messages);
|
|
473
|
+
const shouldExtract = options.extract !== false && extractText !== null;
|
|
469
474
|
this.rememberInScope(transcript, {
|
|
470
475
|
source: "coding-agent-transcript",
|
|
471
476
|
importance: 0.65,
|
|
@@ -476,9 +481,9 @@ export class MnemopiSessionState {
|
|
|
476
481
|
cwd: this.session.sessionManager.getCwd(),
|
|
477
482
|
},
|
|
478
483
|
scope: "bank",
|
|
479
|
-
extract:
|
|
480
|
-
extractEntities:
|
|
481
|
-
extractText,
|
|
484
|
+
extract: shouldExtract,
|
|
485
|
+
extractEntities: shouldExtract,
|
|
486
|
+
extractText: shouldExtract ? extractText : null,
|
|
482
487
|
embedText,
|
|
483
488
|
veracity: "unknown",
|
|
484
489
|
memoryType: "episode",
|
|
@@ -530,22 +535,42 @@ export class MnemopiSessionState {
|
|
|
530
535
|
* otherwise enqueue would report success while leaving the subagent's
|
|
531
536
|
* retained memories unconsolidated until the parent eventually shuts down
|
|
532
537
|
* (PR #2327 review).
|
|
538
|
+
*
|
|
539
|
+
* @param options.full - When true, run `sleepAllSessions` on every owned bank
|
|
540
|
+
* (the full cross-session consolidation used by `/memory enqueue`). When
|
|
541
|
+
* false (the default), run only `sleep` on the current session for a
|
|
542
|
+
* lighter, bounded shutdown pass.
|
|
543
|
+
* @param options.sleep - When false, skips the bank sleep step entirely.
|
|
544
|
+
* Used on the interactive shutdown path so `dispose` does not block on
|
|
545
|
+
* synchronous consolidation of old working rows from previous sessions.
|
|
546
|
+
* @param options.extract - When false, the retained transcript is stored but
|
|
547
|
+
* no LLM fact extraction is scheduled. Used on the interactive shutdown path
|
|
548
|
+
* so `dispose` does not block on a fresh LLM round-trip.
|
|
533
549
|
*/
|
|
534
|
-
async consolidate(): Promise<void> {
|
|
535
|
-
await this.forceRetainCurrentSession();
|
|
550
|
+
async consolidate(options: { full?: boolean; extract?: boolean; sleep?: boolean } = {}): Promise<void> {
|
|
551
|
+
await this.forceRetainCurrentSession({ extract: options.extract });
|
|
536
552
|
for (const memory of this.scoped.owned) {
|
|
537
553
|
await memory.flushExtractions();
|
|
538
|
-
|
|
554
|
+
if (options.sleep === false) continue;
|
|
555
|
+
if (options.full) {
|
|
556
|
+
memory.sleepAllSessions(false);
|
|
557
|
+
} else {
|
|
558
|
+
memory.sleep(false);
|
|
559
|
+
}
|
|
539
560
|
}
|
|
540
561
|
}
|
|
541
562
|
|
|
542
563
|
/**
|
|
543
|
-
* Release the per-session resources. Defaults to running
|
|
544
|
-
* before closing handles
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
564
|
+
* Release the per-session resources. Defaults to running a lighter
|
|
565
|
+
* {@link consolidate} pass before closing handles: it retains the current
|
|
566
|
+
* transcript and flushes in-flight extractions, but skips the synchronous
|
|
567
|
+
* bank sleep so normal session shutdown returns promptly. Full promotion of
|
|
568
|
+
* working memory into long-term storage is still performed by the explicit
|
|
569
|
+
* `/memory enqueue` and backend enqueue paths. Callers that are about to
|
|
570
|
+
* delete the DB files — e.g. `mnemopiBackend.clear` — pass
|
|
571
|
+
* `{ consolidate: false }` to skip the retain/flush pass, since spending
|
|
572
|
+
* tokens on memories that will be wiped on the next line is wasted work
|
|
573
|
+
* (PR #2327 review).
|
|
549
574
|
*
|
|
550
575
|
* `timeoutMs` caps how long the consolidate await blocks the caller
|
|
551
576
|
* (the user-visible `/quit` / `/exit` shutdown path passes this so
|
|
@@ -569,9 +594,11 @@ export class MnemopiSessionState {
|
|
|
569
594
|
closeOwned();
|
|
570
595
|
return;
|
|
571
596
|
}
|
|
572
|
-
const consolidatePromise = this.consolidate().catch(
|
|
573
|
-
|
|
574
|
-
|
|
597
|
+
const consolidatePromise = this.consolidate({ full: false, extract: false, sleep: false }).catch(
|
|
598
|
+
(error: unknown) => {
|
|
599
|
+
logger.warn("Mnemopi: consolidation on dispose failed.", { error: String(error) });
|
|
600
|
+
},
|
|
601
|
+
);
|
|
575
602
|
const { timeoutMs } = options;
|
|
576
603
|
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
577
604
|
const TIMED_OUT = Symbol("mnemopi.dispose.timedOut");
|
|
@@ -19,11 +19,17 @@ interface FinalizableBlock {
|
|
|
19
19
|
/**
|
|
20
20
|
* Monotonic content version for blocks that can still mutate *after*
|
|
21
21
|
* reporting finalized (e.g. `AssistantMessageComponent`: the inline error
|
|
22
|
-
* restored at the next turn's `agent_start`, late tool-result images).
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* restored at the next turn's `agent_start`, late tool-result images). While
|
|
23
|
+
* the block's rows are still on screen (not yet fully committed), the
|
|
24
|
+
* committed-scrollback render bypass replays its previous rows only when the
|
|
25
|
+
* version is unchanged; a bump forces a real render so the TUI's
|
|
26
|
+
* committed-prefix audit can observe and re-anchor the change. Once the rows
|
|
27
|
+
* fully commit to native scrollback they are dropped from the local frame
|
|
28
|
+
* (compacted) — a later mutation no longer recommits on an ordinary frame
|
|
29
|
+
* (immutable history the terminal owns; recommitting would duplicate it) and
|
|
30
|
+
* instead rehydrates on the next destructive full replay
|
|
31
|
+
* ({@link "@oh-my-pi/pi-tui".NativeScrollbackReplay}). Blocks that never
|
|
32
|
+
* mutate post-finalize simply omit the method.
|
|
27
33
|
*/
|
|
28
34
|
getTranscriptBlockVersion?(): number;
|
|
29
35
|
/**
|
|
@@ -124,7 +130,7 @@ interface BlockSegment {
|
|
|
124
130
|
sep: number;
|
|
125
131
|
/** Whether the block reported finalized when this segment was rendered. */
|
|
126
132
|
finalized: boolean;
|
|
127
|
-
/** Safe to drop
|
|
133
|
+
/** Safe to drop from the local frame once its rows fully commit to native scrollback: produced while finalized. */
|
|
128
134
|
compactable: boolean;
|
|
129
135
|
/** Block version observed when this segment was rendered (see {@link FinalizableBlock}). */
|
|
130
136
|
version: number | undefined;
|
|
@@ -453,7 +459,7 @@ export class TranscriptContainer
|
|
|
453
459
|
previous.width === width &&
|
|
454
460
|
previous.generation === this.#generation);
|
|
455
461
|
const contribution = reusable ? previous.contribution : stripPlainBlankEdges(raw);
|
|
456
|
-
const compactable = finalized &&
|
|
462
|
+
const compactable = finalized && previous?.finalized !== false;
|
|
457
463
|
|
|
458
464
|
// Empty (or stripped-to-nothing) children contribute nothing and never
|
|
459
465
|
// affect spacing. An empty still-live child still gates the commit
|
|
@@ -271,10 +271,38 @@ async function resolvePersistedBlobRefs(value: unknown, blobStore: BlobStore, ke
|
|
|
271
271
|
);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Cheap synchronous precheck: does this value's tree contain any `blob:sha256:` string?
|
|
276
|
+
* Early-exits on the first hit and allocates no promises, so blob-free entries skip the
|
|
277
|
+
* async {@link resolvePersistedBlobRefs} descent entirely. Conservative — a blob ref in a
|
|
278
|
+
* non-resolved position still returns true, which only costs an extra (no-op) walk.
|
|
279
|
+
*/
|
|
280
|
+
function containsBlobRef(value: unknown): boolean {
|
|
281
|
+
if (typeof value === "string") return isBlobRef(value);
|
|
282
|
+
if (Array.isArray(value)) {
|
|
283
|
+
for (const item of value) {
|
|
284
|
+
if (containsBlobRef(item)) return true;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
if (typeof value !== "object" || value === null) return false;
|
|
289
|
+
for (const key in value) {
|
|
290
|
+
if (containsBlobRef((value as Record<string, unknown>)[key])) return true;
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
274
295
|
export async function resolveBlobRefsInEntries(entries: FileEntry[], blobStore: BlobStore): Promise<void> {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
296
|
+
const pending: Promise<void>[] = [];
|
|
297
|
+
// Interleave precheck + initiation per entry so a positive entry begins resolution at the same
|
|
298
|
+
// relative point as the old filter+map schedule (no scan-all-first pass that could observe a
|
|
299
|
+
// later entry before an earlier resolution mutates it).
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
if (entry.type === "session") continue;
|
|
302
|
+
if (!containsBlobRef(entry)) continue;
|
|
303
|
+
pending.push(resolvePersistedBlobRefs(entry, blobStore));
|
|
304
|
+
}
|
|
305
|
+
await Promise.all(pending);
|
|
278
306
|
}
|
|
279
307
|
|
|
280
308
|
/**
|
package/src/stt/recorder.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface RecordingHandle {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const isWindows = process.platform === "win32";
|
|
14
|
+
const linuxFFmpegFormats = new Map<string, "pulse" | "alsa">();
|
|
15
|
+
const ffmpegCaptureFlags = ["-hide_banner", "-loglevel", "error", "-nostats"];
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Returns available recording tools in priority order.
|
|
@@ -36,6 +38,36 @@ async function detectWindowsAudioDevice(bin: string): Promise<string> {
|
|
|
36
38
|
return audioDevices[0];
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
async function ffmpegInputArgs(bin: string): Promise<string[]> {
|
|
42
|
+
if (isWindows) {
|
|
43
|
+
return ["-f", "dshow", "-i", `audio=${await detectWindowsAudioDevice(bin)}`];
|
|
44
|
+
}
|
|
45
|
+
if (process.platform === "darwin") {
|
|
46
|
+
return ["-f", "avfoundation", "-i", ":default"];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let format = linuxFFmpegFormats.get(bin);
|
|
50
|
+
if (!format) {
|
|
51
|
+
const result = await $`${bin} -hide_banner -demuxers`.quiet().nothrow();
|
|
52
|
+
if (result.exitCode !== 0) {
|
|
53
|
+
const stderr = result.stderr.toString().trim();
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Could not inspect ffmpeg input formats (code ${result.exitCode}): ${stderr || "(no output)"}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const demuxers = result.stdout.toString();
|
|
59
|
+
if (/^\s*D\s+(?:d\s+)?pulse(?:\s|$)/m.test(demuxers)) {
|
|
60
|
+
format = "pulse";
|
|
61
|
+
} else if (/^\s*D\s+(?:d\s+)?alsa(?:\s|$)/m.test(demuxers)) {
|
|
62
|
+
format = "alsa";
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error("ffmpeg supports neither PulseAudio nor ALSA input on Linux");
|
|
65
|
+
}
|
|
66
|
+
linuxFFmpegFormats.set(bin, format);
|
|
67
|
+
}
|
|
68
|
+
return ["-f", format, "-i", "default"];
|
|
69
|
+
}
|
|
70
|
+
|
|
39
71
|
// ── Recording implementations ──────────────────────────────────────
|
|
40
72
|
|
|
41
73
|
async function startSoxRecording(bin: string, outputPath: string): Promise<RecordingHandle> {
|
|
@@ -44,7 +76,7 @@ async function startSoxRecording(bin: string, outputPath: string): Promise<Recor
|
|
|
44
76
|
|
|
45
77
|
const proc = Bun.spawn([bin, ...inputArgs, "-r", "16000", "-c", "1", "-b", "16", "-t", "wav", outputPath], {
|
|
46
78
|
stdout: "pipe",
|
|
47
|
-
stderr: "
|
|
79
|
+
stderr: "pipe",
|
|
48
80
|
});
|
|
49
81
|
await verifyProcessAlive(proc, "sox");
|
|
50
82
|
return {
|
|
@@ -56,48 +88,24 @@ async function startSoxRecording(bin: string, outputPath: string): Promise<Recor
|
|
|
56
88
|
}
|
|
57
89
|
|
|
58
90
|
async function startFFmpegRecording(bin: string, outputPath: string): Promise<RecordingHandle> {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"-sample_fmt",
|
|
73
|
-
"s16",
|
|
74
|
-
"-y",
|
|
75
|
-
outputPath,
|
|
76
|
-
];
|
|
77
|
-
} else if (process.platform === "darwin") {
|
|
78
|
-
args = [
|
|
79
|
-
bin,
|
|
80
|
-
"-f",
|
|
81
|
-
"avfoundation",
|
|
82
|
-
"-i",
|
|
83
|
-
":default",
|
|
84
|
-
"-ar",
|
|
85
|
-
"16000",
|
|
86
|
-
"-ac",
|
|
87
|
-
"1",
|
|
88
|
-
"-sample_fmt",
|
|
89
|
-
"s16",
|
|
90
|
-
"-y",
|
|
91
|
-
outputPath,
|
|
92
|
-
];
|
|
93
|
-
} else {
|
|
94
|
-
args = [bin, "-f", "pulse", "-i", "default", "-ar", "16000", "-ac", "1", "-sample_fmt", "s16", "-y", outputPath];
|
|
95
|
-
}
|
|
91
|
+
const args = [
|
|
92
|
+
bin,
|
|
93
|
+
...ffmpegCaptureFlags,
|
|
94
|
+
...(await ffmpegInputArgs(bin)),
|
|
95
|
+
"-ar",
|
|
96
|
+
"16000",
|
|
97
|
+
"-ac",
|
|
98
|
+
"1",
|
|
99
|
+
"-sample_fmt",
|
|
100
|
+
"s16",
|
|
101
|
+
"-y",
|
|
102
|
+
outputPath,
|
|
103
|
+
];
|
|
96
104
|
|
|
97
105
|
const proc = Bun.spawn(args, {
|
|
98
106
|
stdin: "pipe",
|
|
99
107
|
stdout: "pipe",
|
|
100
|
-
stderr: "
|
|
108
|
+
stderr: "pipe",
|
|
101
109
|
});
|
|
102
110
|
await verifyProcessAlive(proc, "ffmpeg");
|
|
103
111
|
|
|
@@ -119,7 +127,7 @@ async function startFFmpegRecording(bin: string, outputPath: string): Promise<Re
|
|
|
119
127
|
async function startArecordRecording(bin: string, outputPath: string): Promise<RecordingHandle> {
|
|
120
128
|
const proc = Bun.spawn([bin, "-f", "S16_LE", "-r", "16000", "-c", "1", outputPath], {
|
|
121
129
|
stdout: "pipe",
|
|
122
|
-
stderr: "
|
|
130
|
+
stderr: "pipe",
|
|
123
131
|
});
|
|
124
132
|
await verifyProcessAlive(proc, "arecord");
|
|
125
133
|
return {
|
|
@@ -260,20 +268,19 @@ async function startPowerShellRecording(outputPath: string): Promise<RecordingHa
|
|
|
260
268
|
|
|
261
269
|
// ── Health check ───────────────────────────────────────────────────
|
|
262
270
|
|
|
263
|
-
type RecorderProcess = Subprocess<"ignore" | "pipe", "pipe", "
|
|
271
|
+
type RecorderProcess = Subprocess<"ignore" | "pipe", "pipe", "pipe">;
|
|
264
272
|
|
|
265
273
|
async function verifyProcessAlive(proc: RecorderProcess, tool: string): Promise<void> {
|
|
266
274
|
await Bun.sleep(300);
|
|
267
275
|
|
|
268
276
|
const exited = await Promise.race([proc.exited.then(code => code), Bun.sleep(0).then(() => "running" as const)]);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (proc.stderr && typeof proc.stderr !== "number") {
|
|
273
|
-
stderr = await new Response(proc.stderr as ReadableStream).text();
|
|
274
|
-
}
|
|
275
|
-
throw new Error(`${tool} exited immediately (code ${exited}): ${stderr.trim() || "(no output)"}`);
|
|
277
|
+
if (exited === "running") {
|
|
278
|
+
void proc.stderr.pipeTo(new WritableStream<Uint8Array>()).catch(() => {});
|
|
279
|
+
return;
|
|
276
280
|
}
|
|
281
|
+
|
|
282
|
+
const stderr = await new Response(proc.stderr).text();
|
|
283
|
+
throw new Error(`${tool} exited immediately (code ${exited}): ${stderr.trim() || "(no output)"}`);
|
|
277
284
|
}
|
|
278
285
|
|
|
279
286
|
// ── Public API ─────────────────────────────────────────────────────
|
|
@@ -415,12 +422,18 @@ async function streamingRecorderArgs(recorder: ResolvedRecorder): Promise<string
|
|
|
415
422
|
case "arecord":
|
|
416
423
|
return [bin, "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "raw", "-"];
|
|
417
424
|
case "ffmpeg": {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
425
|
+
return [
|
|
426
|
+
bin,
|
|
427
|
+
...ffmpegCaptureFlags,
|
|
428
|
+
...(await ffmpegInputArgs(bin)),
|
|
429
|
+
"-ar",
|
|
430
|
+
"16000",
|
|
431
|
+
"-ac",
|
|
432
|
+
"1",
|
|
433
|
+
"-f",
|
|
434
|
+
"s16le",
|
|
435
|
+
"pipe:1",
|
|
436
|
+
];
|
|
424
437
|
}
|
|
425
438
|
case "powershell":
|
|
426
439
|
throw new Error("PowerShell recorder cannot stream PCM to a pipe");
|
|
@@ -439,7 +452,7 @@ async function startStreamingRecordingWithRecorder(
|
|
|
439
452
|
): Promise<StreamingRecordingHandle> {
|
|
440
453
|
const args = await streamingRecorderArgs(recorder);
|
|
441
454
|
logger.debug("Starting streaming audio recording", { tool: recorder.tool, bin: recorder.bin });
|
|
442
|
-
const proc = Bun.spawn(args, { stdin: "pipe", stdout: "pipe", stderr: "
|
|
455
|
+
const proc = Bun.spawn(args, { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
|
|
443
456
|
|
|
444
457
|
// Read s16le bytes off stdout, carrying any trailing odd byte across chunk
|
|
445
458
|
// boundaries so a sample is never split. Runs until the process closes stdout.
|
package/src/task/types.ts
CHANGED
|
@@ -106,11 +106,14 @@ export interface SubagentLifecyclePayload {
|
|
|
106
106
|
/** Display cap for a normalized one-line label (roster line, registry `displayName`, prompt field). */
|
|
107
107
|
export const LABEL_MAX = 80;
|
|
108
108
|
|
|
109
|
+
// Keep this explicit: ArkType serializes `unknown` as a boolean subschema, which llama.cpp grammars reject.
|
|
110
|
+
const outputSchemaInputSchema = type("object | boolean | string | null");
|
|
111
|
+
|
|
109
112
|
export const taskItemSchema = type({
|
|
110
113
|
"name?": "string",
|
|
111
114
|
agent: "string = 'task'",
|
|
112
115
|
task: "string",
|
|
113
|
-
"outputSchema?":
|
|
116
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
114
117
|
"schemaMode?": '"permissive" | "strict"',
|
|
115
118
|
"+": "delete",
|
|
116
119
|
});
|
|
@@ -118,7 +121,7 @@ const taskItemSchemaIsolated = type({
|
|
|
118
121
|
"name?": "string",
|
|
119
122
|
agent: "string = 'task'",
|
|
120
123
|
task: "string",
|
|
121
|
-
"outputSchema?":
|
|
124
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
122
125
|
"schemaMode?": '"permissive" | "strict"',
|
|
123
126
|
"isolated?": "boolean",
|
|
124
127
|
"+": "delete",
|
|
@@ -144,7 +147,7 @@ export const taskSchema = type({
|
|
|
144
147
|
"name?": "string",
|
|
145
148
|
agent: "string = 'task'",
|
|
146
149
|
task: "string",
|
|
147
|
-
"outputSchema?":
|
|
150
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
148
151
|
"schemaMode?": '"permissive" | "strict"',
|
|
149
152
|
"isolated?": "boolean",
|
|
150
153
|
"+": "delete",
|
|
@@ -153,7 +156,7 @@ const taskSchemaNoIsolation = type({
|
|
|
153
156
|
"name?": "string",
|
|
154
157
|
agent: "string = 'task'",
|
|
155
158
|
task: "string",
|
|
156
|
-
"outputSchema?":
|
|
159
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
157
160
|
"schemaMode?": '"permissive" | "strict"',
|
|
158
161
|
"+": "delete",
|
|
159
162
|
});
|
|
@@ -197,7 +200,7 @@ function createTaskSchema(options: {
|
|
|
197
200
|
"name?": "string",
|
|
198
201
|
agent,
|
|
199
202
|
task: "string",
|
|
200
|
-
"outputSchema?":
|
|
203
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
201
204
|
"schemaMode?": '"permissive" | "strict"',
|
|
202
205
|
"isolated?": "boolean",
|
|
203
206
|
"+": "delete",
|
|
@@ -212,7 +215,7 @@ function createTaskSchema(options: {
|
|
|
212
215
|
"name?": "string",
|
|
213
216
|
agent,
|
|
214
217
|
task: "string",
|
|
215
|
-
"outputSchema?":
|
|
218
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
216
219
|
"schemaMode?": '"permissive" | "strict"',
|
|
217
220
|
"+": "delete",
|
|
218
221
|
});
|
|
@@ -227,7 +230,7 @@ function createTaskSchema(options: {
|
|
|
227
230
|
"name?": "string",
|
|
228
231
|
agent,
|
|
229
232
|
task: "string",
|
|
230
|
-
"outputSchema?":
|
|
233
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
231
234
|
"schemaMode?": '"permissive" | "strict"',
|
|
232
235
|
"isolated?": "boolean",
|
|
233
236
|
"+": "delete",
|
|
@@ -237,7 +240,7 @@ function createTaskSchema(options: {
|
|
|
237
240
|
"name?": "string",
|
|
238
241
|
agent,
|
|
239
242
|
task: "string",
|
|
240
|
-
"outputSchema?":
|
|
243
|
+
"outputSchema?": outputSchemaInputSchema,
|
|
241
244
|
"schemaMode?": '"permissive" | "strict"',
|
|
242
245
|
"+": "delete",
|
|
243
246
|
});
|