@aeondave/pi-persona 1.17.3 → 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 +44 -36
- package/CHANGELOG.md +76 -1
- package/README.md +23 -13
- package/docs/ARCHITECTURE.md +97 -37
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +33 -8
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +65 -14
- package/package.json +12 -6
- package/src/agents/agent.ts +6 -0
- package/src/bridge.ts +25 -8
- 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 +14 -4
- package/src/core/capabilities.ts +1 -1
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/session-runs.ts +1 -1
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +83 -19
- package/src/engine/async.ts +49 -10
- package/src/engine/child.ts +10 -2
- package/src/engine/inproc.ts +87 -46
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +6 -0
- 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 +45 -17
- 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 +20 -21
- package/src/extension/hooks.ts +952 -938
- package/src/extension/identity.ts +41 -75
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +17 -0
- package/src/extension.ts +2963 -2873
- package/src/loader.ts +11 -3
- package/src/orchestration/flow-journal.ts +7 -1
- package/src/orchestration/flow-run.ts +122 -19
- package/src/orchestration/flow.ts +11 -0
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +44 -6
- 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 +1 -1
- 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/council.ts +40 -23
- package/src/tools/delegate-tool.ts +50 -14
- package/src/tools/delegate.ts +13 -6
- 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.ts +20 -3
- package/src/tools/flow.ts +24 -8
- package/src/tools/intercom-tool.ts +98 -14
- package/src/tools/models.ts +15 -0
- package/src/tools/monitor.ts +20 -1
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +23 -2
- package/src/ui/agent-overlay.ts +11 -3
- package/src/ui/agent-tree.ts +43 -1
- package/src/ui/literal-tool-result.ts +1 -1
- package/src/ui/model-picker.ts +1 -1
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/session-cards.ts +191 -14
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Session identity registration and Pi lifecycle adapter. */
|
|
2
2
|
|
|
3
3
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
4
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { type Static, Type } from "typebox";
|
|
7
7
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
sanitizeIdentityName,
|
|
16
16
|
} from "../core/session-identity.ts";
|
|
17
17
|
import { compactInlineText } from "../ui/presentation.ts";
|
|
18
|
+
import { renderLiteralToolResult } from "../ui/literal-tool-result.ts";
|
|
18
19
|
|
|
19
20
|
export interface IdentityHost {
|
|
20
21
|
capabilities(): EffectiveCapabilities | undefined;
|
|
@@ -34,50 +35,14 @@ const AgentNameParams = Type.Object({
|
|
|
34
35
|
name: Type.String({ minLength: 1, maxLength: 32, description: "A short personal handle, separate from persona and role." }),
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
getHeader?: () => { id?: string } | null;
|
|
40
|
-
getBranch?: () => readonly unknown[];
|
|
41
|
-
getEntries?: () => readonly unknown[];
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
function sessionManager(ctx: ExtensionContext): SessionManagerCompat {
|
|
45
|
-
return (ctx as ExtensionContext & { sessionManager?: SessionManagerCompat }).sessionManager ?? {};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Read the real session id when available. Process fallback is intentionally start-only. */
|
|
49
|
-
function readSessionId(ctx: ExtensionContext, allowProcessFallback: boolean): string | undefined {
|
|
50
|
-
const manager = sessionManager(ctx);
|
|
38
|
+
/** Pi 1.4 supplies both methods. A failed host read is unavailable, never a different owner or branch. */
|
|
39
|
+
function readSession(ctx: ExtensionContext): { id: string; entries: readonly SessionEntry[] } | undefined {
|
|
51
40
|
try {
|
|
52
|
-
const id =
|
|
53
|
-
|
|
41
|
+
const id = ctx.sessionManager.getSessionId();
|
|
42
|
+
return id.trim() ? { id, entries: ctx.sessionManager.getBranch() } : undefined;
|
|
54
43
|
} catch {
|
|
55
|
-
|
|
44
|
+
return undefined;
|
|
56
45
|
}
|
|
57
|
-
try {
|
|
58
|
-
const id = manager.getHeader?.()?.id;
|
|
59
|
-
if (typeof id === "string" && id.trim()) return id;
|
|
60
|
-
} catch {
|
|
61
|
-
/* Compatibility with light-weight session-manager test doubles. */
|
|
62
|
-
}
|
|
63
|
-
return allowProcessFallback ? `process-${process.pid}` : undefined;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function readEntries(ctx: ExtensionContext): readonly unknown[] {
|
|
67
|
-
const manager = sessionManager(ctx);
|
|
68
|
-
try {
|
|
69
|
-
const branch = manager.getBranch?.();
|
|
70
|
-
if (Array.isArray(branch)) return branch;
|
|
71
|
-
} catch {
|
|
72
|
-
/* Fall through to getEntries when a mock or older host has no branch reader. */
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
const entries = manager.getEntries?.();
|
|
76
|
-
if (Array.isArray(entries)) return entries;
|
|
77
|
-
} catch {
|
|
78
|
-
/* Missing persistence only means there is no stored chosen identity to restore. */
|
|
79
|
-
}
|
|
80
|
-
return [];
|
|
81
46
|
}
|
|
82
47
|
|
|
83
48
|
function setIdentityStatus(ctx: ExtensionContext, name: string): void {
|
|
@@ -90,9 +55,7 @@ function setIdentityStatus(ctx: ExtensionContext, name: string): void {
|
|
|
90
55
|
|
|
91
56
|
function activeTools(pi: ExtensionAPI): readonly string[] {
|
|
92
57
|
try {
|
|
93
|
-
|
|
94
|
-
const names = getActiveTools?.call(pi);
|
|
95
|
-
return Array.isArray(names) ? names : [];
|
|
58
|
+
return pi.getActiveTools();
|
|
96
59
|
} catch {
|
|
97
60
|
return [];
|
|
98
61
|
}
|
|
@@ -112,7 +75,7 @@ function messageCustomType(message: AgentMessage): string | undefined {
|
|
|
112
75
|
function hasInboundPeerRequest(messages: readonly AgentMessage[]): boolean {
|
|
113
76
|
return messages.some((message) => {
|
|
114
77
|
const customType = messageCustomType(message);
|
|
115
|
-
return customType === "exocom_received"
|
|
78
|
+
return customType === "exocom_received";
|
|
116
79
|
});
|
|
117
80
|
}
|
|
118
81
|
|
|
@@ -137,39 +100,41 @@ function identityReceipt(name: string, changed: boolean): string {
|
|
|
137
100
|
|
|
138
101
|
export function installIdentity(pi: ExtensionAPI, host: IdentityHost): SessionIdentity {
|
|
139
102
|
let currentSessionId: string | undefined;
|
|
140
|
-
let currentName =
|
|
103
|
+
let currentName = "unavailable";
|
|
141
104
|
let currentChosen = false;
|
|
142
|
-
|
|
143
|
-
|
|
105
|
+
const unavailable = (ctx: ExtensionContext): void => {
|
|
106
|
+
currentSessionId = undefined;
|
|
107
|
+
currentName = "unavailable";
|
|
108
|
+
currentChosen = false;
|
|
109
|
+
setIdentityStatus(ctx, currentName);
|
|
110
|
+
};
|
|
111
|
+
const selectSession = (session: NonNullable<ReturnType<typeof readSession>>): void => {
|
|
112
|
+
if (currentSessionId === session.id) return;
|
|
113
|
+
const stored = findStoredIdentity(session.entries, session.id, host.reservedNames());
|
|
114
|
+
currentSessionId = session.id;
|
|
115
|
+
currentName = stored ?? provisionalIdentityName(session.id);
|
|
116
|
+
currentChosen = stored !== undefined;
|
|
117
|
+
};
|
|
144
118
|
|
|
145
119
|
const identity: SessionIdentity = {
|
|
146
120
|
start(ctx) {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
setIdentityStatus(ctx, currentName);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
currentSessionId = sessionId;
|
|
154
|
-
currentSessionIsFallback = detectedSessionId === undefined;
|
|
155
|
-
const stored = findStoredIdentity(readEntries(ctx), sessionId, host.reservedNames());
|
|
156
|
-
currentName = stored ?? provisionalIdentityName(sessionId);
|
|
157
|
-
currentChosen = stored !== undefined;
|
|
158
|
-
started = true;
|
|
121
|
+
const session = readSession(ctx);
|
|
122
|
+
if (!session) { unavailable(ctx); return; }
|
|
123
|
+
selectSession(session);
|
|
159
124
|
setIdentityStatus(ctx, currentName);
|
|
160
125
|
},
|
|
161
126
|
|
|
162
127
|
rename(raw, ctx) {
|
|
163
128
|
const name = sanitizeIdentityName(raw, host.reservedNames());
|
|
164
129
|
if (!name) throw new Error("identity name is empty, unsafe, generic, or reserved by a persona/agent");
|
|
165
|
-
const
|
|
166
|
-
if (!
|
|
167
|
-
|
|
130
|
+
const session = readSession(ctx);
|
|
131
|
+
if (!session) { unavailable(ctx); throw new Error("identity session is unavailable"); }
|
|
132
|
+
selectSession(session);
|
|
133
|
+
if (currentChosen && currentName === name) return currentName;
|
|
168
134
|
|
|
169
135
|
// Persistence comes first. A failed append therefore cannot leave memory or telemetry
|
|
170
136
|
// claiming a name that is absent from the session history.
|
|
171
|
-
pi.appendEntry(IDENTITY_ENTRY_CUSTOM_TYPE, makeIdentityEntry(
|
|
172
|
-
currentSessionId = sessionId;
|
|
137
|
+
pi.appendEntry(IDENTITY_ENTRY_CUSTOM_TYPE, makeIdentityEntry(session.id, name));
|
|
173
138
|
currentName = name;
|
|
174
139
|
currentChosen = true;
|
|
175
140
|
setIdentityStatus(ctx, currentName);
|
|
@@ -202,17 +167,18 @@ export function installIdentity(pi: ExtensionAPI, host: IdentityHost): SessionId
|
|
|
202
167
|
return { content: [{ type: "text", text }], details: { name, changed } };
|
|
203
168
|
},
|
|
204
169
|
renderCall(args, theme) {
|
|
205
|
-
const name = compactInlineText(args.name, { maxChars: 32 }) || "?";
|
|
170
|
+
const name = compactInlineText(args.name ?? "", { maxChars: 32 }) || "?";
|
|
206
171
|
return new Text(`${theme.fg("toolTitle", theme.bold("Agent Name "))}${theme.fg("accent", name)}`, 0, 0);
|
|
207
172
|
},
|
|
208
|
-
renderResult(result, { expanded }, theme) {
|
|
173
|
+
renderResult(result, { expanded }, theme, context) {
|
|
174
|
+
const failed = context?.isError ?? result.isError ?? false;
|
|
209
175
|
const details = result.details as unknown as { name?: unknown; changed?: unknown } | undefined;
|
|
210
|
-
const name = typeof details?.name === "string" ?
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return new Text(theme.fg(
|
|
176
|
+
const name = !failed && typeof details?.name === "string" ? details.name : "";
|
|
177
|
+
// Failures share the one literal renderer (error heading, bounded preview, full expanded
|
|
178
|
+
// text); a chosen handle is a one-line purpose receipt collapsed and the complete literal
|
|
179
|
+
// receipt expanded.
|
|
180
|
+
if (!name || expanded) return renderLiteralToolResult(result, { expanded }, theme, context);
|
|
181
|
+
return new Text(theme.fg("accent", compactInlineText(identityReceipt(name, details?.changed !== false), { maxChars: 80 })), 0, 0);
|
|
216
182
|
},
|
|
217
183
|
});
|
|
218
184
|
|
|
@@ -228,7 +194,7 @@ export function installIdentity(pi: ExtensionAPI, host: IdentityHost): SessionId
|
|
|
228
194
|
inboundPeer: hasInboundPeerRequest(event.messages),
|
|
229
195
|
...(namingTool === undefined ? {} : { namingTool }),
|
|
230
196
|
};
|
|
231
|
-
const content = buildIdentityContext(currentChosen, contextOptions);
|
|
197
|
+
const content = currentSessionId === undefined ? undefined : buildIdentityContext(currentChosen, contextOptions);
|
|
232
198
|
if (content === undefined) return { messages };
|
|
233
199
|
const contextMessage = {
|
|
234
200
|
role: "custom" as const,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** Session-native terminal archive ownership. Owners are frozen before asynchronous work starts. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { TerminalResultArchive } from "../results/archive.ts";
|
|
5
|
+
import { resultReceipt, type ResultReceipt } from "../results/retrieval.ts";
|
|
6
|
+
import type { TerminalResultInput, TerminalStatus } from "../results/types.ts";
|
|
7
|
+
import type { AgentResult } from "../orchestration/types.ts";
|
|
8
|
+
import { emptyUsage } from "../engine/stream.ts";
|
|
9
|
+
import { type AsyncRun, type AsyncRunTracker } from "../engine/async.ts";
|
|
10
|
+
|
|
11
|
+
export interface CaptureMetadata {
|
|
12
|
+
originRunId?: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
status?: TerminalStatus;
|
|
15
|
+
partial?: string;
|
|
16
|
+
}
|
|
17
|
+
export type CaptureResult = (origin: string, result: AgentResult, meta?: CaptureMetadata) => ResultReceipt;
|
|
18
|
+
interface Owner {
|
|
19
|
+
id: string;
|
|
20
|
+
file: string | undefined;
|
|
21
|
+
manager: ExtensionContext["sessionManager"];
|
|
22
|
+
generation: string;
|
|
23
|
+
archive: TerminalResultArchive;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SessionResults {
|
|
27
|
+
private owner: Owner | undefined;
|
|
28
|
+
private disposed = false;
|
|
29
|
+
private readonly captures = new Map<AsyncRun, CaptureResult>();
|
|
30
|
+
private readonly detach: Array<() => void> = [];
|
|
31
|
+
|
|
32
|
+
constructor(privateApi: Pick<ExtensionAPI, "appendEntry">) {
|
|
33
|
+
this.append = (type, data) => privateApi.appendEntry(type, data);
|
|
34
|
+
}
|
|
35
|
+
private readonly append: (type: string, data: unknown) => void;
|
|
36
|
+
|
|
37
|
+
activate(ctx: ExtensionContext): TerminalResultArchive {
|
|
38
|
+
// A session_shutdown revokes the previous owner; a later session_start in the same process
|
|
39
|
+
// (the documented multi-activation lifecycle) starts a FRESH owner, never a zombie.
|
|
40
|
+
if (this.disposed) {
|
|
41
|
+
this.disposed = false;
|
|
42
|
+
for (const off of this.detach.splice(0)) {
|
|
43
|
+
try { off(); } catch { /* teardown must not block a new activation */ }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const manager = ctx.sessionManager;
|
|
47
|
+
// A context without a session manager (e.g. --no-session / UI-less runs) still gets an
|
|
48
|
+
// honest MEMORY-ONLY owner: bounded runtime retention, no persistence claims.
|
|
49
|
+
const id = manager ? manager.getSessionId() : `memory:${randomUUID()}`;
|
|
50
|
+
const file = manager ? manager.getSessionFile() : undefined;
|
|
51
|
+
const previous = this.owner;
|
|
52
|
+
// File identity owns persisted history. A memory owner additionally needs manager identity.
|
|
53
|
+
if (previous && previous.id === id && previous.file === file && (file !== undefined || previous.manager === manager)) return previous.archive;
|
|
54
|
+
const generation = randomUUID();
|
|
55
|
+
// Freeze all-branch history now. A storage read failure belongs to archive accounting,
|
|
56
|
+
// not to the ordinary work being launched; never retry through a mutable future context.
|
|
57
|
+
let entries: readonly unknown[] = [];
|
|
58
|
+
let historyError: unknown;
|
|
59
|
+
let historyFailed = false;
|
|
60
|
+
try { entries = manager ? manager.getEntries() : []; } catch (error) { historyError = error; historyFailed = true; }
|
|
61
|
+
const owner: Owner = {
|
|
62
|
+
id, file, manager, generation,
|
|
63
|
+
archive: new TerminalResultArchive({
|
|
64
|
+
entries: () => { if (historyFailed) throw historyError; return entries; },
|
|
65
|
+
persistent: file !== undefined,
|
|
66
|
+
append: (type, data) => {
|
|
67
|
+
if (this.owner !== owner || (manager && (manager.getSessionId() !== id || manager.getSessionFile() !== file))) {
|
|
68
|
+
throw new Error("Terminal archive owner was revoked");
|
|
69
|
+
}
|
|
70
|
+
this.append(type, data);
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
this.owner = owner;
|
|
75
|
+
return owner.archive;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get archive(): TerminalResultArchive | undefined { return this.owner?.archive; }
|
|
79
|
+
|
|
80
|
+
/** Bind at logical creation, not at settlement through lastCtx. Each scope has its own origin namespace. */
|
|
81
|
+
bind(scope: string): CaptureResult {
|
|
82
|
+
const owner = this.owner;
|
|
83
|
+
const prefix = `${owner?.generation ?? randomUUID()}:${scope}`;
|
|
84
|
+
return (origin, result, meta = {}) => {
|
|
85
|
+
if (!owner || this.owner !== owner || (owner.manager && (owner.manager.getSessionId() !== owner.id || owner.manager.getSessionFile() !== owner.file))) {
|
|
86
|
+
return { warning: "Terminal report not archived: owning session activation is no longer current." };
|
|
87
|
+
}
|
|
88
|
+
const status = meta.status ?? (result.ok ? "done" : result.failureKind === "abort" ? "stopped" : "failed");
|
|
89
|
+
const output = result.output.trim() ? result.output : meta.partial?.trim() ? meta.partial : result.error ?? "(no output)";
|
|
90
|
+
const input: TerminalResultInput = {
|
|
91
|
+
originKey: `${prefix}:${origin}`, status, agent: result.agent.slice(0, 128), body: output,
|
|
92
|
+
provenance: result.output.trim() ? "result-output" : meta.partial?.trim() ? "partial-output" : result.error ? "error" : "result-output",
|
|
93
|
+
...(meta.originRunId ? { originRunId: meta.originRunId.slice(0, 128) } : {}),
|
|
94
|
+
...(meta.label ? { label: meta.label.slice(0, 256) } : {}),
|
|
95
|
+
...(result.modelUsed ? { model: result.modelUsed.slice(0, 512) } : {}),
|
|
96
|
+
...(result.thinkingRequested ? { thinkingRequested: result.thinkingRequested } : {}),
|
|
97
|
+
...(result.thinkingEffective ? { thinkingEffective: result.thinkingEffective } : {}),
|
|
98
|
+
...(result.error ? { error: result.error.slice(0, 2048) } : {}),
|
|
99
|
+
usage: { input: result.usage.input, output: result.usage.output,
|
|
100
|
+
...(result.usage.cacheRead !== undefined ? { cacheRead: result.usage.cacheRead } : {}),
|
|
101
|
+
...(result.usage.cacheWrite !== undefined ? { cacheWrite: result.usage.cacheWrite } : {}),
|
|
102
|
+
...(result.usage.cost !== undefined ? { cost: result.usage.cost } : {}),
|
|
103
|
+
...(result.usage.turns !== undefined ? { turns: result.usage.turns } : {}),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
try { return resultReceipt(owner.archive.save(input)); }
|
|
107
|
+
catch { return { warning: "Terminal report not archived: storage callback failed." }; }
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Register before the ordinary completion listener. onCreate precedes even a synchronous thunk failure. */
|
|
112
|
+
track(tracker: AsyncRunTracker): void {
|
|
113
|
+
this.detach.push(tracker.onCreate((run) => this.captures.set(run, this.bind("tracker"))));
|
|
114
|
+
this.detach.push(tracker.onComplete((run) => {
|
|
115
|
+
const capture = this.captures.get(run);
|
|
116
|
+
this.captures.delete(run);
|
|
117
|
+
if (!capture) return;
|
|
118
|
+
const result = run.result ?? { agent: run.agent, output: "", usage: emptyUsage(), ok: false, ...(run.error ? { error: run.error } : {}) };
|
|
119
|
+
run.resultReceipt = capture(run.id, result, {
|
|
120
|
+
originRunId: run.id, ...(run.label ? { label: run.label } : {}),
|
|
121
|
+
status: run.status === "running" ? "failed" : run.status,
|
|
122
|
+
partial: run.progress.output,
|
|
123
|
+
});
|
|
124
|
+
if (run.result) run.result.resultReceipt = { ...run.resultReceipt };
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
dispose(): void {
|
|
129
|
+
this.disposed = true;
|
|
130
|
+
this.owner = undefined;
|
|
131
|
+
this.captures.clear();
|
|
132
|
+
for (const off of this.detach.splice(0)) {
|
|
133
|
+
try { off(); } catch { /* best-effort teardown */ }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/extension/shared.ts
CHANGED
|
@@ -172,6 +172,23 @@ export function shouldReportHeartbeatFailure(consecutiveFailures: number): boole
|
|
|
172
172
|
return consecutiveFailures === 1 || consecutiveFailures % 10 === 0;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/** One bounded, terminal-safe cause line for an operator toast. Error `message`s interpolate raw
|
|
176
|
+
* provider/filesystem text (multi-line, ANSI-capable) — every toast that embeds one goes through
|
|
177
|
+
* this so the notification stays a single sanitized line. The untruncated text keeps its other
|
|
178
|
+
* surface (stderr, /doctor, the tool result). */
|
|
179
|
+
export function toastCause(message: string, maxChars = 160): string {
|
|
180
|
+
return compactInlineText(message, { maxChars });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A fair prefix of a name list, bounded for one toast line: `"a, b, c, +N more"`. Discovery
|
|
184
|
+
* reports (collisions, seeded files, installed personas) grow with the user's own directory, so
|
|
185
|
+
* an unbounded `join(", ")` would let the toast wrap over many terminal rows. */
|
|
186
|
+
export function compactNameList(names: readonly string[], max = 8): string {
|
|
187
|
+
const clean = names.map((name) => compactInlineText(name, { maxChars: 80 })).filter((name) => name.length > 0);
|
|
188
|
+
if (clean.length <= max) return clean.join(", ");
|
|
189
|
+
return `${clean.slice(0, max).join(", ")}, +${clean.length - max} more`;
|
|
190
|
+
}
|
|
191
|
+
|
|
175
192
|
export function fenceIntercomOutcome(out: { text: string; details: { action: string; messages?: unknown[] } }, fence: (t: string) => string): string {
|
|
176
193
|
const untrusted = out.details.action === "inbox" && (out.details.messages?.length ?? 0) > 0;
|
|
177
194
|
return untrusted ? fence(out.text) : out.text;
|