@aeondave/pi-persona 1.17.1 → 1.17.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/AGENTS.md +103 -64
- package/CHANGELOG.md +310 -1
- package/README.md +37 -23
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +280 -92
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +52 -17
- package/docs/MONITORS.md +62 -3
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +139 -38
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +15 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +26 -0
- package/src/bridge.ts +28 -10
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +436 -353
- package/src/core/capabilities.ts +1 -1
- package/src/core/config.ts +15 -10
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +95 -27
- package/src/engine/async.ts +213 -155
- package/src/engine/child.ts +12 -4
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +99 -53
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +10 -20
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +47 -18
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/engine.ts +54 -28
- package/src/extension/hooks.ts +952 -868
- package/src/extension/identity.ts +41 -75
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +32 -7
- package/src/extension.ts +2965 -2611
- package/src/loader.ts +11 -3
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +33 -6
- package/src/orchestration/flow-run.ts +160 -32
- package/src/orchestration/flow.ts +20 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -11
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +8 -4
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +108 -66
- package/src/tools/delegate-tool.ts +349 -283
- package/src/tools/delegate.ts +35 -216
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +35 -22
- package/src/tools/flow.ts +103 -30
- package/src/tools/intercom-tool.ts +202 -128
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +34 -9
- package/src/tools/monitor.ts +58 -6
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +34 -15
- package/src/ui/agent-overlay.ts +65 -16
- package/src/ui/agent-tree.ts +127 -19
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +366 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
package/src/loader.ts
CHANGED
|
@@ -120,8 +120,12 @@ export function loadDefinitions(dirs: ScopedDir[]): LoadResult {
|
|
|
120
120
|
const agents: AgentConfig[] = [];
|
|
121
121
|
for (const f of agentMerge.resolved) {
|
|
122
122
|
if (collisionNames.has(f.name)) continue;
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
try {
|
|
124
|
+
const agent = parseAgent(content.get(f.path) ?? "", f.path);
|
|
125
|
+
if (agent) agents.push(agent);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
warnings.push(`${f.path}: ${error instanceof Error ? error.message : String(error)} — skipped`);
|
|
128
|
+
}
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
return { personas, agents, shadowed: [...personaMerge.shadowed, ...agentMerge.shadowed], collisions, warnings };
|
|
@@ -212,7 +216,11 @@ export function loadTeams(files: string[]): Record<string, RosterMember[]> {
|
|
|
212
216
|
} catch {
|
|
213
217
|
continue;
|
|
214
218
|
}
|
|
215
|
-
|
|
219
|
+
try {
|
|
220
|
+
Object.assign(merged, parseTeams(content));
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw new Error(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
223
|
+
}
|
|
216
224
|
}
|
|
217
225
|
return merged;
|
|
218
226
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MONITOR_MAX_ARGS, MONITOR_MAX_ARG_CHARS, MONITOR_MAX_ARGV_CHARS,
|
|
3
|
+
MONITOR_MAX_COMMAND_CHARS, MONITOR_MAX_EVENTS, MONITOR_MAX_LABEL_CHARS,
|
|
4
|
+
type MonitorEntry, type MonitorEvent, type MonitorOutput,
|
|
5
|
+
} from "../core/monitor.ts";
|
|
6
|
+
|
|
7
|
+
export const MONITOR_LOG_ENTRY = "pi-persona-monitor-log";
|
|
8
|
+
export const MONITOR_LOG_MAX_STREAM_CHARS = 16 * 1024;
|
|
9
|
+
export const MONITOR_LOG_MAX_TEXT_CHARS = 16 * 1024;
|
|
10
|
+
export const MONITOR_LOG_MAX_RECORDS = 64;
|
|
11
|
+
export const MONITOR_LOG_MAX_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
export interface MonitorLogRecord {
|
|
14
|
+
version: 1;
|
|
15
|
+
entry: MonitorEntry;
|
|
16
|
+
outcome: "running" | "exit" | "error" | "timeout" | "cancelled" | "ended";
|
|
17
|
+
/** Last semantic event's text (not an interleaved raw stream). */
|
|
18
|
+
text: string;
|
|
19
|
+
stdout: string;
|
|
20
|
+
stderr: string;
|
|
21
|
+
truncated: boolean;
|
|
22
|
+
storageWarning?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MonitorArchiveDeps {
|
|
26
|
+
/** Only the owning session's entries; never a global session/history lookup. */
|
|
27
|
+
entries: () => readonly unknown[];
|
|
28
|
+
/** Session-native custom data, NOT a model-visible custom message. */
|
|
29
|
+
append: (customType: string, data: unknown) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const BUDGET_WARNING = "Monitor log persistence unavailable: session archive budget exhausted; this log is retained only in bounded runtime history.";
|
|
33
|
+
const APPEND_WARNING = "Monitor log persistence unavailable: appending the session entry failed; this log is retained only in bounded runtime history.";
|
|
34
|
+
const HISTORY_WARNING = "Monitor log history unavailable: owning-session entries could not be read; persistence is disabled to preserve the session budget.";
|
|
35
|
+
|
|
36
|
+
function object(value: unknown): value is Record<string, unknown> {
|
|
37
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nonempty(value: unknown, limit: number): value is string {
|
|
41
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= limit && !value.includes("\0");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function onlyKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
45
|
+
return Object.keys(value).every((key) => keys.includes(key));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validEntry(value: unknown): value is MonitorEntry {
|
|
49
|
+
if (!object(value) || !onlyKeys(value, ["id", "label", "command", "args", "cwd", "mode", "startedAtEpochMs", "expiresAtEpochMs", "maxEvents"])) return false;
|
|
50
|
+
return nonempty(value.id, 512)
|
|
51
|
+
&& nonempty(value.label, MONITOR_MAX_LABEL_CHARS)
|
|
52
|
+
&& nonempty(value.command, MONITOR_MAX_COMMAND_CHARS)
|
|
53
|
+
&& nonempty(value.cwd, 32_768)
|
|
54
|
+
&& (value.mode === "output" || value.mode === "exit")
|
|
55
|
+
&& Array.isArray(value.args) && value.args.length <= MONITOR_MAX_ARGS
|
|
56
|
+
&& Array.from(value.args).every((arg: unknown) => typeof arg === "string" && arg.length <= MONITOR_MAX_ARG_CHARS && !arg.includes("\0"))
|
|
57
|
+
&& value.args.reduce((sum: number, arg: string) => sum + arg.length, 0) <= MONITOR_MAX_ARGV_CHARS
|
|
58
|
+
&& typeof value.startedAtEpochMs === "number" && Number.isFinite(value.startedAtEpochMs) && value.startedAtEpochMs >= 0
|
|
59
|
+
&& typeof value.expiresAtEpochMs === "number" && Number.isFinite(value.expiresAtEpochMs) && value.expiresAtEpochMs >= value.startedAtEpochMs
|
|
60
|
+
&& typeof value.maxEvents === "number" && Number.isInteger(value.maxEvents) && value.maxEvents >= 1 && value.maxEvents <= MONITOR_MAX_EVENTS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function copyEntry(entry: MonitorEntry): MonitorEntry {
|
|
64
|
+
return {
|
|
65
|
+
id: entry.id, label: entry.label, command: entry.command, args: [...entry.args],
|
|
66
|
+
cwd: entry.cwd, mode: entry.mode, startedAtEpochMs: entry.startedAtEpochMs,
|
|
67
|
+
expiresAtEpochMs: entry.expiresAtEpochMs, maxEvents: entry.maxEvents,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function copyRecord(record: MonitorLogRecord): MonitorLogRecord {
|
|
72
|
+
return {
|
|
73
|
+
version: 1, entry: copyEntry(record.entry), outcome: record.outcome,
|
|
74
|
+
text: record.text, stdout: record.stdout, stderr: record.stderr, truncated: record.truncated,
|
|
75
|
+
...(record.storageWarning === undefined ? {} : { storageWarning: record.storageWarning }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Validate custom-entry DATA for historical operator rendering; never returns input references.
|
|
80
|
+
* Unknown fields are rejected so serialized metadata cannot hide outside the archive budget. */
|
|
81
|
+
export function parseMonitorLogRecord(value: unknown): MonitorLogRecord | undefined {
|
|
82
|
+
try {
|
|
83
|
+
return parseRecord(value);
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseRecord(value: unknown): MonitorLogRecord | undefined {
|
|
90
|
+
if (!object(value) || !onlyKeys(value, ["version", "entry", "outcome", "text", "stdout", "stderr", "truncated", "storageWarning"])
|
|
91
|
+
|| value.version !== 1 || !validEntry(value.entry)) return undefined;
|
|
92
|
+
if (value.outcome !== "exit" && value.outcome !== "error" && value.outcome !== "timeout"
|
|
93
|
+
&& value.outcome !== "cancelled" && value.outcome !== "ended") return undefined;
|
|
94
|
+
if (typeof value.text !== "string" || value.text.length > MONITOR_LOG_MAX_TEXT_CHARS
|
|
95
|
+
|| typeof value.stdout !== "string" || value.stdout.length > MONITOR_LOG_MAX_STREAM_CHARS
|
|
96
|
+
|| typeof value.stderr !== "string" || value.stderr.length > MONITOR_LOG_MAX_STREAM_CHARS
|
|
97
|
+
|| typeof value.truncated !== "boolean"
|
|
98
|
+
|| (value.storageWarning !== undefined && (typeof value.storageWarning !== "string" || value.storageWarning.length > 1024))) return undefined;
|
|
99
|
+
return copyRecord(value as unknown as MonitorLogRecord);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** UTF-8 uses at most three bytes per UTF-16 code unit, AFTER JSON escaping.
|
|
103
|
+
* Reserve extra space for the host's custom-entry envelope. Deliberately conservative:
|
|
104
|
+
* this bounds the archive's serialized records, not arbitrary host/session metadata. */
|
|
105
|
+
function storageCost(record: unknown): number {
|
|
106
|
+
return (JSON.stringify(record) ?? "null").length * 3 + 256;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Bounded, session-native raw logs. No files, processes, timers, wakes, or model messages.
|
|
110
|
+
* Feed only admitted MonitorManager lifecycles (at most MONITOR_MAX_ACTIVE running entries).
|
|
111
|
+
* Completed records are oldest-first; the most recent 64 survive in runtime memory.
|
|
112
|
+
* Hydration runs once, so construct a fresh archive when the owning session changes.
|
|
113
|
+
* History never restores running monitors. A removed observer has no cause information:
|
|
114
|
+
* cancellation, permission revocation, shutdown and synchronous start failure all map to cancelled.
|
|
115
|
+
*/
|
|
116
|
+
export class MonitorArchive {
|
|
117
|
+
private readonly deps: MonitorArchiveDeps;
|
|
118
|
+
private readonly active = new Map<string, MonitorLogRecord>();
|
|
119
|
+
private readonly completed = new Map<string, MonitorLogRecord>();
|
|
120
|
+
private hydrated = false;
|
|
121
|
+
private persistedCount = 0;
|
|
122
|
+
private persistedBytes = 0;
|
|
123
|
+
private historyWarning: string | undefined;
|
|
124
|
+
|
|
125
|
+
constructor(deps: MonitorArchiveDeps) {
|
|
126
|
+
this.deps = deps;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
armed(entry: MonitorEntry): void {
|
|
130
|
+
this.hydrate();
|
|
131
|
+
if (this.active.has(entry.id) || this.completed.has(entry.id)) return;
|
|
132
|
+
this.active.set(entry.id, {
|
|
133
|
+
version: 1, entry: copyEntry(entry), outcome: "running", text: "", stdout: "", stderr: "", truncated: false,
|
|
134
|
+
...(this.historyWarning === undefined ? {} : { storageWarning: this.historyWarning }),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
output({ id, stream, text }: MonitorOutput): void {
|
|
139
|
+
const record = this.active.get(id);
|
|
140
|
+
if (!record || !text) return;
|
|
141
|
+
const current = record[stream];
|
|
142
|
+
if (current.length + text.length > MONITOR_LOG_MAX_STREAM_CHARS) record.truncated = true;
|
|
143
|
+
// Slice the chunk BEFORE concatenation: a huge callback must not create a huge copy.
|
|
144
|
+
record[stream] = text.length >= MONITOR_LOG_MAX_STREAM_CHARS
|
|
145
|
+
? text.slice(-MONITOR_LOG_MAX_STREAM_CHARS)
|
|
146
|
+
: (current + text).slice(-MONITOR_LOG_MAX_STREAM_CHARS);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
event(event: MonitorEvent): void {
|
|
150
|
+
const record = this.active.get(event.id);
|
|
151
|
+
if (!record) return;
|
|
152
|
+
record.text = event.text.slice(0, MONITOR_LOG_MAX_TEXT_CHARS);
|
|
153
|
+
if (event.text.length > MONITOR_LOG_MAX_TEXT_CHARS) record.truncated = true;
|
|
154
|
+
if (!event.terminal) return;
|
|
155
|
+
record.outcome = event.kind === "output" ? "ended" : event.kind;
|
|
156
|
+
this.finish(record);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
removed(entry: MonitorEntry): void {
|
|
160
|
+
const record = this.active.get(entry.id);
|
|
161
|
+
if (!record) return;
|
|
162
|
+
record.outcome = "cancelled";
|
|
163
|
+
this.finish(record);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
result(id: string): MonitorLogRecord | undefined {
|
|
167
|
+
this.hydrate();
|
|
168
|
+
const record = this.active.get(id) ?? this.completed.get(id);
|
|
169
|
+
return record ? copyRecord(record) : undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
list(): MonitorLogRecord[] {
|
|
173
|
+
this.hydrate();
|
|
174
|
+
return [...this.completed.values(), ...this.active.values()].map(copyRecord);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private hydrate(): void {
|
|
178
|
+
if (this.hydrated) return;
|
|
179
|
+
this.hydrated = true;
|
|
180
|
+
try {
|
|
181
|
+
const entries = this.deps.entries();
|
|
182
|
+
if (!Array.isArray(entries)) throw new Error("invalid session entries");
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
try {
|
|
185
|
+
if (!object(entry) || entry.type !== "custom" || entry.customType !== MONITOR_LOG_ENTRY) continue;
|
|
186
|
+
// Charge even unknown/malformed versions and duplicate ids: ignoring a record
|
|
187
|
+
// for retrieval does not remove its bytes from an append-only session.
|
|
188
|
+
this.persistedCount++;
|
|
189
|
+
this.persistedBytes += storageCost(entry.data);
|
|
190
|
+
const record = parseMonitorLogRecord(entry.data);
|
|
191
|
+
if (record) this.remember(record);
|
|
192
|
+
} catch {
|
|
193
|
+
// An unaccountable entry cannot safely reset the lifetime budget.
|
|
194
|
+
this.historyWarning = HISTORY_WARNING;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// Without a trustworthy baseline, appending could silently overrun the session quota.
|
|
199
|
+
this.historyWarning = HISTORY_WARNING;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private finish(record: MonitorLogRecord): void {
|
|
204
|
+
// Settle BEFORE calling injected persistence; duplicate/reentrant callbacks are inert.
|
|
205
|
+
this.active.delete(record.entry.id);
|
|
206
|
+
this.remember(record);
|
|
207
|
+
if (this.historyWarning !== undefined) {
|
|
208
|
+
record.storageWarning = this.historyWarning;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const cost = storageCost(record);
|
|
212
|
+
if (this.persistedCount >= MONITOR_LOG_MAX_RECORDS || this.persistedBytes + cost > MONITOR_LOG_MAX_BYTES) {
|
|
213
|
+
record.storageWarning = BUDGET_WARNING;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Reserve even when append throws: it may have committed before reporting failure.
|
|
217
|
+
// This also bounds repeated failing append attempts over the runtime's lifetime.
|
|
218
|
+
this.persistedCount++;
|
|
219
|
+
this.persistedBytes += cost;
|
|
220
|
+
try {
|
|
221
|
+
this.deps.append(MONITOR_LOG_ENTRY, copyRecord(record));
|
|
222
|
+
} catch {
|
|
223
|
+
record.storageWarning = APPEND_WARNING;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private remember(record: MonitorLogRecord): void {
|
|
228
|
+
this.completed.delete(record.entry.id);
|
|
229
|
+
this.completed.set(record.entry.id, record);
|
|
230
|
+
while (this.completed.size > MONITOR_LOG_MAX_RECORDS) {
|
|
231
|
+
const oldest = this.completed.keys().next().value;
|
|
232
|
+
if (oldest !== undefined) this.completed.delete(oldest);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/monitor/session.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
/** Monitor events use the same idle follow-up delivery as timer and Intercom. */
|
|
2
|
-
import { MonitorManager, type MonitorEvent } from "../core/monitor.ts";
|
|
2
|
+
import { MonitorManager, type MonitorEntry, type MonitorEvent, type MonitorOutput } from "../core/monitor.ts";
|
|
3
3
|
import { fenceUntrusted, stripTerminalControls } from "../core/fence.ts";
|
|
4
4
|
import { IdleCoalescingNotifier } from "../engine/async.ts";
|
|
5
5
|
import { startMonitorProcess } from "./process.ts";
|
|
6
6
|
|
|
7
|
+
/** A passive view of the monitor lifecycle (the agent tree's monitor rows). Every `armed` is followed
|
|
8
|
+
* by exactly one of: a terminal `event`, or `removed` (cancel, permission loss, shutdown, start failure).
|
|
9
|
+
* Observation never changes admission or delivery. */
|
|
10
|
+
export interface MonitorObserver {
|
|
11
|
+
armed(entry: MonitorEntry): void;
|
|
12
|
+
event(event: MonitorEvent): void;
|
|
13
|
+
removed(entry: MonitorEntry): void;
|
|
14
|
+
/** Raw process output for bounded local views/storage only; never a wake. */
|
|
15
|
+
output?(output: MonitorOutput): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
interface MonitorSessionDeps {
|
|
19
|
+
/** Activation-unique prefix when results are persisted across reloads. */
|
|
20
|
+
idPrefix?: string;
|
|
8
21
|
isIdle(): boolean;
|
|
9
22
|
canRun(): boolean;
|
|
10
23
|
deliver(text: string): void;
|
|
11
24
|
start?: typeof startMonitorProcess;
|
|
25
|
+
observe?: MonitorObserver;
|
|
12
26
|
}
|
|
13
27
|
|
|
14
28
|
export function createMonitorSession(d: MonitorSessionDeps) {
|
|
@@ -31,7 +45,13 @@ export function createMonitorSession(d: MonitorSessionDeps) {
|
|
|
31
45
|
});
|
|
32
46
|
const manager = new MonitorManager({
|
|
33
47
|
...timerDeps, now: () => Date.now(), start: d.start ?? startMonitorProcess,
|
|
48
|
+
...(d.idPrefix ? { idPrefix: d.idPrefix } : {}),
|
|
49
|
+
...(d.observe ? { onArmed: (entry: MonitorEntry) => d.observe?.armed(entry), onRemoved: (entry: MonitorEntry) => d.observe?.removed(entry) } : {}),
|
|
50
|
+
onOutput: (output) => { d.observe?.output?.(output); },
|
|
34
51
|
onEvent: (event) => {
|
|
52
|
+
// Observed BEFORE the permission gate: a terminal event has already left the manager, so the
|
|
53
|
+
// `cancelAll` below could never announce it, and its row would stay on screen forever.
|
|
54
|
+
d.observe?.event(event);
|
|
35
55
|
if (!d.canRun()) { cancelAll(); return; }
|
|
36
56
|
const oldest = notifier.peekPending()[0];
|
|
37
57
|
if (notifier.peekPending().length >= 64 && oldest) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Flow journal (v0.5) — JSONL resume. Each completed phase appends one line; on a later
|
|
3
3
|
* run the journal is read back into a resume map (phase id → output) so done phases are
|
|
4
|
-
* skipped. Lines whose `hash` doesn't match the current
|
|
5
|
-
*
|
|
4
|
+
* skipped. Lines whose `hash` doesn't match the current run hash (`flowRunHash`: flow + task +
|
|
5
|
+
* cwd) are ignored (another flow version, task or project), and a phase recorded `ok:false` is
|
|
6
|
+
* NOT resumed (it re-runs). `parseJournal`
|
|
6
7
|
* and the filename derivation are pure (tested); the read/write helpers are thin `fs` glue.
|
|
7
8
|
*/
|
|
8
9
|
|
|
@@ -24,8 +25,9 @@ const PATH_HOSTILE = /[/\\<>:"|?*\x00-\x1f]/g;
|
|
|
24
25
|
* name as a filename can be; the `flow@hash` half is what actually keeps journals apart, since
|
|
25
26
|
* it covers the whole spec (the name included). Two names that encode alike — `ci:quick` and
|
|
26
27
|
* `ci?quick` — are therefore still two different specs with two different hashes and two
|
|
27
|
-
* different files, and so is a name long enough to be truncated here.
|
|
28
|
-
*
|
|
28
|
+
* different files, and so is a name long enough to be truncated here. Runs of ONE flow on another
|
|
29
|
+
* task or project share this file, and a truncated hash could collide: either way every LINE
|
|
30
|
+
* carries the full run hash (`flowRunHash`), so a resume reads only its own. */
|
|
29
31
|
export function journalFileName(spec: FlowSpec): string {
|
|
30
32
|
return `${encodeFlowName(spec.name)}.${flowHash(spec).slice(0, 8)}.journal.jsonl`;
|
|
31
33
|
}
|
|
@@ -38,7 +40,7 @@ function encodeFlowName(name: string): string {
|
|
|
38
40
|
return encoded || "flow";
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
/** Fold a JSONL journal into a resume map for the given
|
|
43
|
+
/** Fold a JSONL journal into a resume map for the given run hash. Later lines win, so a
|
|
42
44
|
* phase that failed then succeeded ends up resumed; a phase still failed is left to re-run.
|
|
43
45
|
* A `gateApproved` line marks a checkpoint already approved so a resume doesn't re-prompt. */
|
|
44
46
|
export function parseJournal(content: string, hash: string): Record<string, ResumedResult> {
|
|
@@ -57,7 +59,13 @@ export function parseJournal(content: string, hash: string): Record<string, Resu
|
|
|
57
59
|
if (e.hash !== hash || typeof e.phase !== "string") continue;
|
|
58
60
|
if (e.ok) {
|
|
59
61
|
const prior = out[e.phase];
|
|
60
|
-
out[e.phase] = {
|
|
62
|
+
out[e.phase] = {
|
|
63
|
+
agent: e.phase, output: e.output ?? "", usage: emptyUsage(), ok: true,
|
|
64
|
+
...(typeof e.resultRef === "string" ? { resultRef: e.resultRef } : {}),
|
|
65
|
+
...(e.resultVersion === 1 ? { resultVersion: 1 as const } :
|
|
66
|
+
e.resultRef !== undefined || e.resultVersion !== undefined
|
|
67
|
+
? { refUnavailable: "unsupported or missing archived result journal version" } : {}),
|
|
68
|
+
};
|
|
61
69
|
// Preserve an approval from either this line or an earlier line for the same phase.
|
|
62
70
|
if (e.gateApproved || prior?.gateApproved) out[e.phase]!.gateApproved = true;
|
|
63
71
|
} else delete out[e.phase];
|
|
@@ -74,6 +82,25 @@ export function readJournal(path: string, hash: string): Record<string, ResumedR
|
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
/** A finished run leaves the file: drop ITS lines (unreadable ones too), keep every other run's
|
|
86
|
+
* resumable progress, and remove the file once nothing is left. Best-effort. */
|
|
87
|
+
export function clearJournal(path: string, hash: string): void {
|
|
88
|
+
try {
|
|
89
|
+
const others = fs.readFileSync(path, "utf8").split("\n").filter((line) => {
|
|
90
|
+
try {
|
|
91
|
+
const e = JSON.parse(line) as FlowJournalEntry | null;
|
|
92
|
+
return !!e && typeof e === "object" && e.hash !== hash;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
if (others.length === 0) fs.rmSync(path, { force: true });
|
|
98
|
+
else fs.writeFileSync(path, `${others.join("\n")}\n`);
|
|
99
|
+
} catch {
|
|
100
|
+
/* best effort — a journal that cannot be cleaned is only a stale resume candidate */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
77
104
|
/** An appending writer — one JSONL line per completed phase. Best-effort. */
|
|
78
105
|
export function journalWriter(path: string): (entry: FlowJournalEntry) => void {
|
|
79
106
|
return (entry) => {
|