@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.11
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 +40 -0
- package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-d9xenpn9.md} +40 -0
- package/dist/cli.js +3025 -3016
- package/dist/types/capability/mcp.d.ts +11 -0
- package/dist/types/capability/skill.d.ts +6 -0
- package/dist/types/cli/command-help.d.ts +3 -0
- package/dist/types/commands/share.d.ts +25 -0
- package/dist/types/commit/agentic/index.d.ts +3 -1
- package/dist/types/commit/execute.d.ts +32 -0
- package/dist/types/commit/index.d.ts +2 -1
- package/dist/types/commit/pipeline.d.ts +7 -1
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +1 -34
- package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
- package/dist/types/discovery/agent-plugins.d.ts +1 -0
- package/dist/types/discovery/contained-path.d.ts +34 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -2
- package/dist/types/extensibility/skills.d.ts +5 -0
- package/dist/types/mcp/transports/header-policy.d.ts +45 -0
- package/dist/types/mcp/types.d.ts +15 -0
- package/dist/types/modes/components/agent-hub.d.ts +2 -0
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -2
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
- package/dist/types/secrets/index.d.ts +23 -1
- package/dist/types/session/agent-session-events.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/prewalk.d.ts +3 -1
- package/dist/types/session/session-tools.d.ts +8 -0
- package/dist/types/session/turn-recovery.d.ts +7 -2
- package/dist/types/task/index.d.ts +2 -0
- package/dist/types/tools/shell-tokenize.d.ts +16 -0
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +4 -1
- package/src/capability/mcp.ts +11 -0
- package/src/capability/skill.ts +6 -0
- package/src/cli/command-help.ts +4 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli-commands.ts +5 -0
- package/src/commands/commit.ts +16 -3
- package/src/commands/share.ts +71 -0
- package/src/commit/agentic/index.ts +39 -21
- package/src/commit/execute.ts +56 -0
- package/src/commit/index.ts +2 -1
- package/src/commit/pipeline.ts +20 -7
- package/src/config/model-discovery.ts +1 -1
- package/src/config/model-registry.ts +21 -1
- package/src/config/settings-schema.ts +2 -33
- package/src/config/settings.ts +46 -0
- package/src/discovery/agent-plugin-format.ts +551 -0
- package/src/discovery/agent-plugins.ts +341 -0
- package/src/discovery/claude-plugins.ts +21 -5
- package/src/discovery/contained-path.ts +78 -0
- package/src/discovery/helpers.ts +23 -9
- package/src/discovery/index.ts +1 -0
- package/src/discovery/omp-plugins.ts +20 -7
- package/src/exec/non-interactive-env.ts +1 -0
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +78 -14
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +12 -0
- package/src/extensibility/plugins/marketplace/manager.ts +2 -1
- package/src/extensibility/shared-events.ts +3 -2
- package/src/extensibility/skills.ts +9 -0
- package/src/internal-urls/skill-protocol.ts +14 -0
- package/src/internal-urls/vault-protocol.ts +2 -2
- package/src/launch/client.ts +11 -1
- package/src/launch/protocol.ts +7 -2
- package/src/main.ts +3 -2
- package/src/mcp/config.ts +3 -0
- package/src/mcp/manager.ts +12 -9
- package/src/mcp/transports/header-policy.ts +95 -0
- package/src/mcp/transports/http.ts +35 -52
- package/src/mcp/transports/sse.ts +20 -27
- package/src/mcp/types.ts +15 -0
- package/src/modes/acp/acp-agent.ts +15 -4
- package/src/modes/components/agent-dashboard.ts +2 -0
- package/src/modes/components/agent-hub.ts +5 -0
- package/src/modes/components/agent-transcript-viewer.ts +3 -0
- package/src/modes/components/chat-transcript-builder.ts +3 -0
- package/src/modes/components/custom-editor.ts +0 -9
- package/src/modes/components/status-line/component.ts +1 -0
- package/src/modes/components/status-line/segments.ts +6 -4
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/tool-execution.ts +13 -16
- package/src/modes/components/tree-selector.ts +62 -3
- package/src/modes/controllers/command-controller.ts +4 -1
- package/src/modes/controllers/event-controller.ts +15 -15
- package/src/modes/controllers/input-controller.ts +20 -2
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/utils/transcript-render-helpers.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/sdk.ts +6 -49
- package/src/secrets/index.ts +52 -1
- package/src/session/agent-session-events.ts +2 -2
- package/src/session/agent-session.ts +33 -14
- package/src/session/messages.ts +1 -0
- package/src/session/prewalk.ts +57 -17
- package/src/session/session-context.ts +3 -5
- package/src/session/session-handoff.ts +5 -1
- package/src/session/session-tools.ts +37 -3
- package/src/session/todo-tracker.ts +11 -1
- package/src/session/turn-recovery.ts +94 -72
- package/src/slash-commands/builtin-registry.ts +12 -8
- package/src/task/index.ts +21 -6
- package/src/tools/bash-skill-urls.ts +15 -0
- package/src/tools/bash.ts +10 -11
- package/src/tools/debug.ts +1 -1
- package/src/tools/jtd-to-json-schema.ts +123 -21
- package/src/tools/read.ts +5 -4
- package/src/tools/shell-tokenize.ts +98 -0
- package/src/web/search/providers/exa.ts +1 -1
|
@@ -286,12 +286,20 @@ class TreeList implements Component {
|
|
|
286
286
|
|
|
287
287
|
// Apply filter mode
|
|
288
288
|
let passesFilter = true;
|
|
289
|
-
// Entry types hidden in default view (settings/bookkeeping)
|
|
289
|
+
// Entry types hidden in default view (settings/bookkeeping). These carry
|
|
290
|
+
// no conversation content, so the tree only shows them in "all" mode.
|
|
290
291
|
const isSettingsEntry =
|
|
291
292
|
entry.type === "label" ||
|
|
292
293
|
entry.type === "custom" ||
|
|
293
294
|
entry.type === "model_change" ||
|
|
294
|
-
entry.type === "thinking_level_change"
|
|
295
|
+
entry.type === "thinking_level_change" ||
|
|
296
|
+
entry.type === "service_tier_change" ||
|
|
297
|
+
entry.type === "title_change" ||
|
|
298
|
+
entry.type === "credential_pin" ||
|
|
299
|
+
entry.type === "session_init" ||
|
|
300
|
+
entry.type === "ttsr_injection" ||
|
|
301
|
+
entry.type === "mode_change" ||
|
|
302
|
+
entry.type === "reset_boundary";
|
|
295
303
|
|
|
296
304
|
switch (this.#filterMode) {
|
|
297
305
|
case "user-only":
|
|
@@ -390,6 +398,34 @@ class TreeList implements Component {
|
|
|
390
398
|
case "label":
|
|
391
399
|
parts.push("label", entry.label ?? "");
|
|
392
400
|
break;
|
|
401
|
+
case "service_tier_change":
|
|
402
|
+
parts.push("service tier");
|
|
403
|
+
if (entry.serviceTier) {
|
|
404
|
+
const serviceTier = entry.serviceTier;
|
|
405
|
+
for (const family in serviceTier) {
|
|
406
|
+
const tier = serviceTier[family as keyof typeof serviceTier];
|
|
407
|
+
if (tier) parts.push(family, tier);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
case "title_change":
|
|
412
|
+
parts.push("title", entry.title);
|
|
413
|
+
break;
|
|
414
|
+
case "mode_change":
|
|
415
|
+
parts.push("mode", entry.mode);
|
|
416
|
+
break;
|
|
417
|
+
case "credential_pin":
|
|
418
|
+
parts.push("credential pin", entry.provider);
|
|
419
|
+
break;
|
|
420
|
+
case "ttsr_injection":
|
|
421
|
+
parts.push("ttsr injection", ...entry.injectedRules);
|
|
422
|
+
break;
|
|
423
|
+
case "reset_boundary":
|
|
424
|
+
parts.push("reset boundary");
|
|
425
|
+
break;
|
|
426
|
+
case "session_init":
|
|
427
|
+
parts.push("session init");
|
|
428
|
+
break;
|
|
393
429
|
}
|
|
394
430
|
|
|
395
431
|
return parts.join(" ");
|
|
@@ -658,8 +694,31 @@ class TreeList implements Component {
|
|
|
658
694
|
case "label":
|
|
659
695
|
result = theme.fg("dim", `[label: ${entry.label ?? "(cleared)"}]`);
|
|
660
696
|
break;
|
|
697
|
+
case "service_tier_change": {
|
|
698
|
+
// Per-family map, or null when the session went back to the default.
|
|
699
|
+
const tiers = entry.serviceTier
|
|
700
|
+
? Object.entries(entry.serviceTier)
|
|
701
|
+
.map(([family, tier]) => `${family}:${tier}`)
|
|
702
|
+
.join(" ")
|
|
703
|
+
: "(default)";
|
|
704
|
+
result = theme.fg("dim", `[service tier: ${tiers}]`);
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
case "title_change":
|
|
708
|
+
result = theme.fg("dim", `[title: ${normalize(entry.title)}]`);
|
|
709
|
+
break;
|
|
710
|
+
case "mode_change":
|
|
711
|
+
result = theme.fg("dim", `[mode: ${entry.mode}]`);
|
|
712
|
+
break;
|
|
713
|
+
case "credential_pin":
|
|
714
|
+
result = theme.fg("dim", `[credential pin: ${entry.provider}]`);
|
|
715
|
+
break;
|
|
661
716
|
default:
|
|
662
|
-
|
|
717
|
+
// Bookkeeping entries with nothing worth spelling out still get their
|
|
718
|
+
// type. A row that renders to the empty string is worse than a
|
|
719
|
+
// useless one: it draws as a bare bullet with no way to tell what it
|
|
720
|
+
// is or why the tree has a gap in it.
|
|
721
|
+
result = theme.fg("dim", `[${entry.type.replaceAll("_", " ")}]`);
|
|
663
722
|
}
|
|
664
723
|
|
|
665
724
|
return isSelected ? theme.bold(result) : result;
|
|
@@ -1436,7 +1436,10 @@ export class CommandController {
|
|
|
1436
1436
|
}
|
|
1437
1437
|
} catch (error) {
|
|
1438
1438
|
const message = error instanceof Error ? error.message : String(error);
|
|
1439
|
-
|
|
1439
|
+
// `session.handoff()` normalizes genuine cancellations to this exact message; a
|
|
1440
|
+
// provider error (even one named AbortError) is re-thrown verbatim so it surfaces
|
|
1441
|
+
// as a real failure instead of a false "cancelled".
|
|
1442
|
+
if (message === "Handoff cancelled") {
|
|
1440
1443
|
this.ctx.showError("Handoff cancelled");
|
|
1441
1444
|
} else {
|
|
1442
1445
|
this.ctx.showError(`Handoff failed: ${message}`);
|
|
@@ -1073,6 +1073,7 @@ export class EventController {
|
|
|
1073
1073
|
content.name,
|
|
1074
1074
|
renderArgs,
|
|
1075
1075
|
{
|
|
1076
|
+
useBuiltInRenderer: this.ctx.viewSession.hasBuiltInTool(content.name),
|
|
1076
1077
|
snapshots: getFileSnapshotStore(this.ctx.viewSession),
|
|
1077
1078
|
clipboard: getEditClipboard(this.ctx.viewSession),
|
|
1078
1079
|
showImages: settings.get("terminal.showImages"),
|
|
@@ -1322,6 +1323,7 @@ export class EventController {
|
|
|
1322
1323
|
event.toolName,
|
|
1323
1324
|
event.args,
|
|
1324
1325
|
{
|
|
1326
|
+
useBuiltInRenderer: this.ctx.viewSession.hasBuiltInTool(event.toolName),
|
|
1325
1327
|
snapshots: getFileSnapshotStore(this.ctx.viewSession),
|
|
1326
1328
|
clipboard: getEditClipboard(this.ctx.viewSession),
|
|
1327
1329
|
showImages: settings.get("terminal.showImages"),
|
|
@@ -1920,21 +1922,19 @@ export class EventController {
|
|
|
1920
1922
|
this.ctx.retryLoader = undefined;
|
|
1921
1923
|
this.ctx.statusContainer.disposeChildren();
|
|
1922
1924
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
} else {
|
|
1937
|
-
this.#clearRetrySupersededAssistantComponents();
|
|
1925
|
+
let appliedRetryUpdate = false;
|
|
1926
|
+
for (const retryError of event.retryErrors ?? []) {
|
|
1927
|
+
const component = this.#takeRetrySupersededAssistantComponent(retryError.persistenceKey);
|
|
1928
|
+
if (!component) continue;
|
|
1929
|
+
component.applyRetryRecovery(retryError.retryRecovery);
|
|
1930
|
+
if (this.#pinnedErrorComponent === component) this.#pinnedErrorComponent = undefined;
|
|
1931
|
+
appliedRetryUpdate = true;
|
|
1932
|
+
}
|
|
1933
|
+
if (appliedRetryUpdate || (event.retryErrors?.length ?? 0) > 0) {
|
|
1934
|
+
this.ctx.clearPinnedError();
|
|
1935
|
+
}
|
|
1936
|
+
this.#clearRetrySupersededAssistantComponents();
|
|
1937
|
+
if (!event.success) {
|
|
1938
1938
|
this.ctx.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
|
|
1939
1939
|
}
|
|
1940
1940
|
this.#ensureWorkingLoaderWhileStreaming();
|
|
@@ -13,6 +13,7 @@ import { renderSegmentTrack } from "../../modes/components/segment-track";
|
|
|
13
13
|
import { StrippedToolCallsPlaceholder } from "../../modes/components/stripped-tool-calls-placeholder";
|
|
14
14
|
import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
|
|
15
15
|
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
16
|
+
import { TreeSelectorComponent } from "../../modes/components/tree-selector";
|
|
16
17
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
17
18
|
import { materializeImageReferenceLinks, shiftImageMarkers } from "../../modes/image-references";
|
|
18
19
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
@@ -177,6 +178,7 @@ export class InputController {
|
|
|
177
178
|
#focusedPasteListenerInstalled = false;
|
|
178
179
|
#btwBranchListenerInstalled = false;
|
|
179
180
|
#btwCopyListenerInstalled = false;
|
|
181
|
+
#expandToolsListenerInstalled = false;
|
|
180
182
|
// Tap counter for the double-← gesture; reset whenever a quiet gap
|
|
181
183
|
// (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
|
|
182
184
|
// #detectLeftDoubleTap.
|
|
@@ -278,6 +280,24 @@ export class InputController {
|
|
|
278
280
|
return { consume: true };
|
|
279
281
|
});
|
|
280
282
|
}
|
|
283
|
+
if (!this.#expandToolsListenerInstalled) {
|
|
284
|
+
this.#expandToolsListenerInstalled = true;
|
|
285
|
+
// `app.tools.expand` (Ctrl+O) toggles the transcript's tool-output
|
|
286
|
+
// preview. It must fire regardless of focus so a truncated edit stays
|
|
287
|
+
// expandable while an approval prompt / select dialog holds keyboard
|
|
288
|
+
// focus (#7837). Defers when the main transcript is not the active
|
|
289
|
+
// surface (a fullscreen/anchored overlay — agent hub, transcript
|
|
290
|
+
// viewer, log viewer, model picker) or when the focused component
|
|
291
|
+
// rebinds Ctrl+O for its own use (the tree selector's filter cycle).
|
|
292
|
+
this.ctx.ui.addInputListener(data => {
|
|
293
|
+
if (!this.ctx.keybindings.matches(data, "app.tools.expand")) return undefined;
|
|
294
|
+
if (this.ctx.ui.hasOverlay()) return undefined;
|
|
295
|
+
if (this.ctx.ui.getFocused() instanceof TreeSelectorComponent && matchesKey(data, "ctrl+o"))
|
|
296
|
+
return undefined;
|
|
297
|
+
this.toggleToolOutputExpansion();
|
|
298
|
+
return { consume: true };
|
|
299
|
+
});
|
|
300
|
+
}
|
|
281
301
|
this.ctx.editor.onEscape = () => {
|
|
282
302
|
// Side-channel panels are the topmost view. Esc dismisses them before
|
|
283
303
|
// touching loop mode, maintenance, or the underlying main turn.
|
|
@@ -456,8 +476,6 @@ export class InputController {
|
|
|
456
476
|
this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
|
|
457
477
|
);
|
|
458
478
|
this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
|
|
459
|
-
this.ctx.editor.setActionKeys("app.tools.expand", this.ctx.keybindings.getKeys("app.tools.expand"));
|
|
460
|
-
this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
|
|
461
479
|
this.ctx.editor.setActionKeys(
|
|
462
480
|
"app.tools.toggleVisibility",
|
|
463
481
|
this.ctx.keybindings.getKeys("app.tools.toggleVisibility"),
|
|
@@ -2026,6 +2026,7 @@ export class SelectorController {
|
|
|
2026
2026
|
remote: this.ctx.collabGuest?.hubRemote,
|
|
2027
2027
|
ui: this.ctx.ui,
|
|
2028
2028
|
getTool: name => this.ctx.session.getToolByName(name),
|
|
2029
|
+
isBuiltInTool: name => this.ctx.session.hasBuiltInTool(name),
|
|
2029
2030
|
getMessageRenderer: type => this.ctx.session.extensionRunner?.getMessageRenderer(type),
|
|
2030
2031
|
cwd: this.ctx.sessionManager.getCwd(),
|
|
2031
2032
|
hideThinkingBlock: () => this.ctx.effectiveHideThinkingBlock,
|
|
@@ -211,13 +211,15 @@ function sanitizeRecoveredRetryNote(note: string): string {
|
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
213
|
* Resolve the turn-ending assistant error presentation, if any.
|
|
214
|
-
* Silent and user-interrupt aborts yield no label. Recovered
|
|
215
|
-
*
|
|
214
|
+
* Silent and user-interrupt aborts yield no label. Recovered retry attempts
|
|
215
|
+
* render a compact note; attempts superseded by an exhausted budget are hidden
|
|
216
|
+
* while the final terminal error keeps its full presentation.
|
|
216
217
|
*/
|
|
217
218
|
export function resolveAssistantErrorPresentation(
|
|
218
219
|
message: AssistantAgentMessage,
|
|
219
220
|
retryAttempt = 0,
|
|
220
221
|
): AssistantErrorPresentation {
|
|
222
|
+
if (message.retryRecovery?.status === "superseded") return { kind: "none" };
|
|
221
223
|
if (message.retryRecovery?.status === "recovered") {
|
|
222
224
|
return {
|
|
223
225
|
kind: "compact-recovered",
|
|
@@ -500,6 +500,7 @@ export class UiHelpers {
|
|
|
500
500
|
content.name,
|
|
501
501
|
renderArgs,
|
|
502
502
|
{
|
|
503
|
+
useBuiltInRenderer: this.ctx.viewSession.hasBuiltInTool(content.name),
|
|
503
504
|
snapshots: getFileSnapshotStore(this.ctx.viewSession),
|
|
504
505
|
clipboard: getEditClipboard(this.ctx.viewSession),
|
|
505
506
|
showImages: settings.get("terminal.showImages"),
|
package/src/sdk.ts
CHANGED
|
@@ -123,18 +123,12 @@ import lateDiagnosticTemplate from "./prompts/tools/lsp-late-diagnostic.md" with
|
|
|
123
123
|
import { AgentLifecycleManager } from "./registry/agent-lifecycle";
|
|
124
124
|
import { type AgentRef, AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
|
|
125
125
|
import {
|
|
126
|
-
|
|
127
|
-
collectEnvSecrets,
|
|
126
|
+
buildSecretObfuscator,
|
|
128
127
|
deobfuscateSessionContext,
|
|
129
128
|
deobfuscateToolArguments,
|
|
130
|
-
getExistingSecretPlaceholderKey,
|
|
131
|
-
getSecretPlaceholderKey,
|
|
132
|
-
getSecretPlaceholderKeySync,
|
|
133
|
-
loadSecrets,
|
|
134
129
|
obfuscateMessages,
|
|
135
130
|
obfuscateProviderContext,
|
|
136
|
-
SecretObfuscator,
|
|
137
|
-
secretEntriesNeedPlaceholderKey,
|
|
131
|
+
type SecretObfuscator,
|
|
138
132
|
} from "./secrets";
|
|
139
133
|
import { AgentSession, type InitialRetryFallbackState, type PlanYolo, type Prewalk } from "./session/agent-session";
|
|
140
134
|
import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config";
|
|
@@ -1039,7 +1033,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
|
|
|
1039
1033
|
success: event.success,
|
|
1040
1034
|
attempt: event.attempt,
|
|
1041
1035
|
finalError: event.finalError,
|
|
1042
|
-
|
|
1036
|
+
retryErrors: event.retryErrors,
|
|
1043
1037
|
},
|
|
1044
1038
|
ctx,
|
|
1045
1039
|
),
|
|
@@ -1377,46 +1371,9 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
1377
1371
|
|
|
1378
1372
|
// Load and create secret obfuscator early so resumed session state and prompt warnings
|
|
1379
1373
|
// reflect actual loaded secrets, not just the setting toggle.
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
const envEntries = collectEnvSecrets();
|
|
1384
|
-
// Built-in credential-pattern entries come last so user-configured entries
|
|
1385
|
-
// (plain literals, custom regexes) take precedence in the scan order.
|
|
1386
|
-
const allEntries = [...envEntries, ...fileEntries, ...builtinCredentialSecretEntries()];
|
|
1387
|
-
// Only CONFIGURED entries force startup key creation: a configured
|
|
1388
|
-
// obfuscate-mode secret — or a default (no custom `replacement`)
|
|
1389
|
-
// replace-mode regex whose key-derived idempotent fallback marker needs a
|
|
1390
|
-
// stable key across restarts (see `secretEntryNeedsPlaceholderKey`) —
|
|
1391
|
-
// mints placeholders as soon as the obfuscator is built. The built-in
|
|
1392
|
-
// credential-pattern entry matches dynamically, so it resolves the
|
|
1393
|
-
// persisted key lazily on first match instead of creating the key file
|
|
1394
|
-
// for every secrets.enabled session; a session whose content never
|
|
1395
|
-
// contains a credential-shaped token must not require the key, otherwise a
|
|
1396
|
-
// headless run on an unwritable default config root pays for a feature it
|
|
1397
|
-
// does not use.
|
|
1398
|
-
const needsPlaceholderKey = secretEntriesNeedPlaceholderKey([...envEntries, ...fileEntries]);
|
|
1399
|
-
const explicitAgentDir = options.agentDir;
|
|
1400
|
-
const placeholderKey = needsPlaceholderKey
|
|
1401
|
-
? await getSecretPlaceholderKey(explicitAgentDir)
|
|
1402
|
-
: await getExistingSecretPlaceholderKey(explicitAgentDir);
|
|
1403
|
-
if (allEntries.length > 0) {
|
|
1404
|
-
obfuscator = new SecretObfuscator(
|
|
1405
|
-
allEntries,
|
|
1406
|
-
placeholderKey ?? (() => getSecretPlaceholderKeySync(explicitAgentDir)),
|
|
1407
|
-
);
|
|
1408
|
-
}
|
|
1409
|
-
if (obfuscator?.hasSecrets() !== true && placeholderKey !== undefined) {
|
|
1410
|
-
// No configured entry produced an active secret (e.g. only ignored short
|
|
1411
|
-
// plain entries, or no entries at all), but a persisted key exists. Build a
|
|
1412
|
-
// redaction-only obfuscator so a tool read of the key file does not ship the
|
|
1413
|
-
// reusable HMAC key to the provider.
|
|
1414
|
-
obfuscator = new SecretObfuscator(
|
|
1415
|
-
[{ type: "plain", mode: "replace", content: placeholderKey }],
|
|
1416
|
-
placeholderKey,
|
|
1417
|
-
);
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1374
|
+
const obfuscator: SecretObfuscator | undefined = settings.get("secrets.enabled")
|
|
1375
|
+
? await buildSecretObfuscator(cwd, agentDir, options.agentDir)
|
|
1376
|
+
: undefined;
|
|
1420
1377
|
const secretsEnabled = obfuscator?.hasSecrets() === true;
|
|
1421
1378
|
|
|
1422
1379
|
// An abnormal process exit after a non-terminal message tail is durable
|
package/src/secrets/index.ts
CHANGED
|
@@ -4,7 +4,13 @@ import * as path from "node:path";
|
|
|
4
4
|
import { SENSITIVE_TOKEN_RE } from "@oh-my-pi/pi-ai/providers/transform-messages";
|
|
5
5
|
import { getSecretPlaceholderKeyPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { YAML } from "bun";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
regexHasUnresolvableShortMatchFallback,
|
|
9
|
+
type SecretEntry,
|
|
10
|
+
SecretObfuscator,
|
|
11
|
+
sanitizeSecretFriendlyName,
|
|
12
|
+
secretEntriesNeedPlaceholderKey,
|
|
13
|
+
} from "./obfuscator";
|
|
8
14
|
import { compileSecretRegex } from "./regex";
|
|
9
15
|
|
|
10
16
|
const PLACEHOLDER_KEY_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
@@ -223,6 +229,51 @@ export function builtinCredentialSecretEntries(): SecretEntry[] {
|
|
|
223
229
|
];
|
|
224
230
|
}
|
|
225
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Build the session secret obfuscator from every configured source: secrets.yml
|
|
234
|
+
* (project + global), secret-shaped environment variables, and the built-in
|
|
235
|
+
* credential patterns. Callers gate on `secrets.enabled`.
|
|
236
|
+
*
|
|
237
|
+
* Only CONFIGURED entries force startup key creation: a configured
|
|
238
|
+
* obfuscate-mode secret — or a default (no custom `replacement`) replace-mode
|
|
239
|
+
* regex whose key-derived idempotent fallback marker needs a stable key across
|
|
240
|
+
* restarts (see `secretEntryNeedsPlaceholderKey`) — mints placeholders as soon
|
|
241
|
+
* as the obfuscator is built. The built-in credential-pattern entry matches
|
|
242
|
+
* dynamically, so it resolves the persisted key lazily on first match instead
|
|
243
|
+
* of creating the key file for every secrets-enabled session.
|
|
244
|
+
*
|
|
245
|
+
* When no configured entry produced an active secret but a persisted key
|
|
246
|
+
* exists, returns a redaction-only obfuscator so a tool read of the key file
|
|
247
|
+
* does not ship the reusable HMAC key to the provider. Returns undefined when
|
|
248
|
+
* there is nothing to protect.
|
|
249
|
+
*
|
|
250
|
+
* `keyDir` is the explicit agent dir override for the placeholder-key file
|
|
251
|
+
* (default XDG/agent location when omitted).
|
|
252
|
+
*/
|
|
253
|
+
export async function buildSecretObfuscator(
|
|
254
|
+
cwd: string,
|
|
255
|
+
agentDir: string,
|
|
256
|
+
keyDir?: string,
|
|
257
|
+
): Promise<SecretObfuscator | undefined> {
|
|
258
|
+
const fileEntries = await logger.time("loadSecrets", loadSecrets, cwd, agentDir);
|
|
259
|
+
const envEntries = collectEnvSecrets();
|
|
260
|
+
// Built-in credential-pattern entries come last so user-configured entries
|
|
261
|
+
// (plain literals, custom regexes) take precedence in the scan order.
|
|
262
|
+
const allEntries = [...envEntries, ...fileEntries, ...builtinCredentialSecretEntries()];
|
|
263
|
+
const needsPlaceholderKey = secretEntriesNeedPlaceholderKey([...envEntries, ...fileEntries]);
|
|
264
|
+
const placeholderKey = needsPlaceholderKey
|
|
265
|
+
? await getSecretPlaceholderKey(keyDir)
|
|
266
|
+
: await getExistingSecretPlaceholderKey(keyDir);
|
|
267
|
+
let obfuscator: SecretObfuscator | undefined;
|
|
268
|
+
if (allEntries.length > 0) {
|
|
269
|
+
obfuscator = new SecretObfuscator(allEntries, placeholderKey ?? (() => getSecretPlaceholderKeySync(keyDir)));
|
|
270
|
+
}
|
|
271
|
+
if (obfuscator?.hasSecrets() !== true && placeholderKey !== undefined) {
|
|
272
|
+
obfuscator = new SecretObfuscator([{ type: "plain", mode: "replace", content: placeholderKey }], placeholderKey);
|
|
273
|
+
}
|
|
274
|
+
return obfuscator;
|
|
275
|
+
}
|
|
276
|
+
|
|
226
277
|
async function loadSecretsFile(filePath: string): Promise<SecretEntry[]> {
|
|
227
278
|
try {
|
|
228
279
|
const text = await Bun.file(filePath).text();
|
|
@@ -2,7 +2,7 @@ import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
|
3
3
|
import type { Effort } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import type { Rule } from "../capability/rule";
|
|
5
|
-
import type {
|
|
5
|
+
import type { RetryErrorUpdate } from "../extensibility/shared-events";
|
|
6
6
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
7
7
|
import type { ConfiguredThinkingLevel } from "../thinking";
|
|
8
8
|
import type { TodoItem } from "../tools/todo";
|
|
@@ -43,7 +43,7 @@ export type AgentSessionEvent =
|
|
|
43
43
|
success: boolean;
|
|
44
44
|
attempt: number;
|
|
45
45
|
finalError?: string;
|
|
46
|
-
|
|
46
|
+
retryErrors?: RetryErrorUpdate[];
|
|
47
47
|
}
|
|
48
48
|
| { type: "retry_fallback_applied"; from: string; to: string; role: string }
|
|
49
49
|
| { type: "retry_fallback_succeeded"; model: string; role: string }
|
|
@@ -304,7 +304,7 @@ import {
|
|
|
304
304
|
USER_INTERRUPT_LABEL,
|
|
305
305
|
} from "./messages";
|
|
306
306
|
import { ModelControls, type ModelControlsHost } from "./model-controls";
|
|
307
|
-
import { PrewalkCoordinator, type PrewalkCoordinatorHost } from "./prewalk";
|
|
307
|
+
import { isPrewalkPlanNudge, PrewalkCoordinator, type PrewalkCoordinatorHost } from "./prewalk";
|
|
308
308
|
import {
|
|
309
309
|
isAdvisorCard,
|
|
310
310
|
isDisplayableQueuedMessage,
|
|
@@ -881,8 +881,8 @@ export class AgentSession {
|
|
|
881
881
|
/**
|
|
882
882
|
* Arm prewalk outside the normal startup path so an explicit slash command starts immediately.
|
|
883
883
|
*/
|
|
884
|
-
armPrewalk(target: Model, thinkingLevel?: ConfiguredThinkingLevel):
|
|
885
|
-
this.#prewalk.arm(target, thinkingLevel);
|
|
884
|
+
armPrewalk(target: Model, thinkingLevel?: ConfiguredThinkingLevel): boolean {
|
|
885
|
+
return this.#prewalk.arm(target, thinkingLevel);
|
|
886
886
|
}
|
|
887
887
|
|
|
888
888
|
/** Validate the active plan artifact and shape an `xd://propose` result for review-mode hosts. */
|
|
@@ -2489,14 +2489,17 @@ export class AgentSession {
|
|
|
2489
2489
|
const persistMessageEnd = () => {
|
|
2490
2490
|
// Check if this is a hook/custom message
|
|
2491
2491
|
if (event.message.role === "hookMessage" || event.message.role === "custom") {
|
|
2492
|
-
//
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2492
|
+
// Prewalk's plan nudge is a one-run steering instruction. Persisting it would
|
|
2493
|
+
// resurrect the consumed prompt on resume, fork, or any context rebuild.
|
|
2494
|
+
if (!isPrewalkPlanNudge(event.message)) {
|
|
2495
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
2496
|
+
event.message.customType,
|
|
2497
|
+
event.message.content,
|
|
2498
|
+
event.message.display,
|
|
2499
|
+
event.message.details,
|
|
2500
|
+
event.message.attribution ?? "agent",
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2500
2503
|
if (event.message.role === "custom" && event.message.customType === "ttsr-injection") {
|
|
2501
2504
|
this.#ttsr.markInjectedFromDetails(event.message.details);
|
|
2502
2505
|
}
|
|
@@ -3017,11 +3020,23 @@ export class AgentSession {
|
|
|
3017
3020
|
}
|
|
3018
3021
|
this.#beginInFlight();
|
|
3019
3022
|
try {
|
|
3020
|
-
await this.#recovery.maybeRestoreRetryFallbackPrimary();
|
|
3023
|
+
const reverted = await this.#recovery.maybeRestoreRetryFallbackPrimary();
|
|
3021
3024
|
if (signal.aborted || this.#isDisposed) {
|
|
3022
3025
|
this.#skipAgentContinue("post-restore-unavailable", options);
|
|
3023
3026
|
return;
|
|
3024
3027
|
}
|
|
3028
|
+
// A cooldown-expiry revert can drop the active window below the
|
|
3029
|
+
// accumulated context. The user-prompt path re-checks context after
|
|
3030
|
+
// the revert via runPrePromptCompactionIfNeeded; the auto-continue
|
|
3031
|
+
// path must do the same so agent.continue() never sends a
|
|
3032
|
+
// predictably oversized request to the reverted (smaller) model.
|
|
3033
|
+
if (reverted) {
|
|
3034
|
+
await this.#maintenance.runPrePromptCompactionIfNeeded([]);
|
|
3035
|
+
if (signal.aborted || this.#isDisposed) {
|
|
3036
|
+
this.#skipAgentContinue("post-restore-unavailable", options);
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3025
3040
|
if (this.settings.get("retry.usageAwareFallback")) {
|
|
3026
3041
|
if (!(await this.#runQueuedUsageAwarePreflight(signal))) {
|
|
3027
3042
|
this.#skipAgentContinue("session-unavailable", options);
|
|
@@ -3440,7 +3455,7 @@ export class AgentSession {
|
|
|
3440
3455
|
success: event.success,
|
|
3441
3456
|
attempt: event.attempt,
|
|
3442
3457
|
finalError: event.finalError,
|
|
3443
|
-
|
|
3458
|
+
retryErrors: event.retryErrors,
|
|
3444
3459
|
});
|
|
3445
3460
|
} else if (event.type === "ttsr_triggered") {
|
|
3446
3461
|
await this.#extensionRunner.emit({ type: "ttsr_triggered", rules: event.rules });
|
|
@@ -5273,11 +5288,13 @@ export class AgentSession {
|
|
|
5273
5288
|
|
|
5274
5289
|
if (result?.systemPrompt !== undefined) {
|
|
5275
5290
|
baseXdevCatalogDelivered = false;
|
|
5276
|
-
this.
|
|
5291
|
+
this.#tools.setTurnSystemPromptOverride(result.systemPrompt);
|
|
5277
5292
|
} else {
|
|
5293
|
+
this.#tools.clearTurnSystemPromptOverride();
|
|
5278
5294
|
this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
|
|
5279
5295
|
}
|
|
5280
5296
|
} else {
|
|
5297
|
+
this.#tools.clearTurnSystemPromptOverride();
|
|
5281
5298
|
this.agent.setSystemPrompt(beforeAgentStartSystemPrompt);
|
|
5282
5299
|
}
|
|
5283
5300
|
|
|
@@ -5342,6 +5359,8 @@ export class AgentSession {
|
|
|
5342
5359
|
await this.#waitForPostPromptRecovery(generation);
|
|
5343
5360
|
}
|
|
5344
5361
|
} finally {
|
|
5362
|
+
// The per-turn before_agent_start override lives only for this turn.
|
|
5363
|
+
this.#tools.clearTurnSystemPromptOverride();
|
|
5345
5364
|
this.#usagePreflightReadyForNextModelCall = false;
|
|
5346
5365
|
this.#endInFlight();
|
|
5347
5366
|
}
|
package/src/session/messages.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { formatOutputNotice } from "../tools/output-meta";
|
|
|
42
42
|
export const SKILL_PROMPT_MESSAGE_TYPE = "skill-prompt";
|
|
43
43
|
export const LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE = "lsp-late-diagnostic";
|
|
44
44
|
export const BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE = "background-tan-dispatch";
|
|
45
|
+
export const PREWALK_PLAN_MESSAGE_TYPE = "prewalk-plan";
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Logs provider-error turns so their actual cause is available outside the
|
package/src/session/prewalk.ts
CHANGED
|
@@ -14,11 +14,16 @@ import { type ConfiguredThinkingLevel, prewalkWouldBeNoop } from "../thinking";
|
|
|
14
14
|
import type { PlanProposalHandler } from "../tools/resolve";
|
|
15
15
|
import { ToolError } from "../tools/tool-errors";
|
|
16
16
|
import type { PlanYolo, Prewalk } from "./agent-session-types";
|
|
17
|
+
import { PREWALK_PLAN_MESSAGE_TYPE } from "./messages";
|
|
17
18
|
import type { SessionManager } from "./session-manager";
|
|
18
19
|
|
|
19
|
-
const PREWALK_PLAN_MESSAGE_TYPE = "prewalk-plan";
|
|
20
20
|
const PREWALK_CONTINUE_MESSAGE_TYPE = "prewalk-continue";
|
|
21
21
|
const PREWALK_CHECKLIST_MESSAGE_TYPE = "prewalk-checklist";
|
|
22
|
+
|
|
23
|
+
/** Hidden plan steering is consumed within the live run and must not reappear after a context rebuild. */
|
|
24
|
+
export function isPrewalkPlanNudge(message: AgentMessage): boolean {
|
|
25
|
+
return message.role === "custom" && message.customType === PREWALK_PLAN_MESSAGE_TYPE;
|
|
26
|
+
}
|
|
22
27
|
const PREWALK_ACTION_TOOLS: Record<string, true> = {
|
|
23
28
|
edit: true,
|
|
24
29
|
write: true,
|
|
@@ -99,10 +104,40 @@ export class PrewalkCoordinator {
|
|
|
99
104
|
return this.#prewalk;
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
#isNoop(prewalk: Prewalk): boolean {
|
|
108
|
+
return prewalkWouldBeNoop(
|
|
109
|
+
this.#host.model(),
|
|
110
|
+
this.#host.configuredThinkingLevel(),
|
|
111
|
+
prewalk.target,
|
|
112
|
+
prewalk.thinkingLevel,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#clearPrewalkState(): void {
|
|
117
|
+
this.#prewalk = undefined;
|
|
118
|
+
this.#planInjected = false;
|
|
119
|
+
this.#continuePending = false;
|
|
120
|
+
this.#todoSeen = false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#disarmNoop(prewalk: Prewalk): void {
|
|
124
|
+
this.#clearPrewalkState();
|
|
125
|
+
this.#host.emitNotice(
|
|
126
|
+
"info",
|
|
127
|
+
`Prewalk: target ${prewalk.target.provider}/${prewalk.target.id} already matches the active model and thinking level; nothing to switch.`,
|
|
128
|
+
"prewalk",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
102
132
|
/** Advances the one-way prewalk switch at a completed assistant-turn boundary. */
|
|
103
133
|
async advanceAtTurnEnd(liveMessages: AgentMessage[], context: AgentTurnEndContext | undefined): Promise<void> {
|
|
104
134
|
const prewalk = this.#prewalk;
|
|
105
135
|
if (!prewalk || context?.message.role !== "assistant") return;
|
|
136
|
+
if (this.#isNoop(prewalk)) {
|
|
137
|
+
this.#scrubPlanNudge(liveMessages);
|
|
138
|
+
this.#disarmNoop(prewalk);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
106
141
|
if (context.toolResults.some(result => result.toolName === "todo" && !result.isError)) this.#todoSeen = true;
|
|
107
142
|
|
|
108
143
|
const hasToolResults = context.toolResults.length > 0;
|
|
@@ -147,18 +182,12 @@ export class PrewalkCoordinator {
|
|
|
147
182
|
}
|
|
148
183
|
this.#scrubPlanNudge(liveMessages);
|
|
149
184
|
const target = prewalk.target;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.#prewalk = undefined;
|
|
153
|
-
this.#host.emitNotice(
|
|
154
|
-
"info",
|
|
155
|
-
`Prewalk: target ${target.provider}/${target.id} already matches the active model and thinking level; nothing to switch.`,
|
|
156
|
-
"prewalk",
|
|
157
|
-
);
|
|
185
|
+
if (this.#isNoop(prewalk)) {
|
|
186
|
+
this.#disarmNoop(prewalk);
|
|
158
187
|
return;
|
|
159
188
|
}
|
|
160
189
|
await this.#host.setModelTemporary(target, prewalk.thinkingLevel, { ephemeral: true });
|
|
161
|
-
this.#
|
|
190
|
+
this.#clearPrewalkState();
|
|
162
191
|
this.#host.emitNotice(
|
|
163
192
|
"info",
|
|
164
193
|
`Prewalk: switched to ${target.provider}/${target.id} after first ${action.toolName} call.`,
|
|
@@ -175,18 +204,29 @@ export class PrewalkCoordinator {
|
|
|
175
204
|
}
|
|
176
205
|
|
|
177
206
|
/** Arms a prewalk immediately for an explicit slash-command request. */
|
|
178
|
-
arm(target: Model, thinkingLevel?: ConfiguredThinkingLevel):
|
|
179
|
-
|
|
207
|
+
arm(target: Model, thinkingLevel?: ConfiguredThinkingLevel): boolean {
|
|
208
|
+
const active = this.#prewalk;
|
|
209
|
+
if (active) {
|
|
180
210
|
this.#host.emitNotice(
|
|
181
211
|
"info",
|
|
182
|
-
`Prewalk: already armed for ${
|
|
212
|
+
`Prewalk: already armed for ${active.target.provider}/${active.target.id}, waiting for the first edit/write.`,
|
|
183
213
|
"prewalk",
|
|
184
214
|
);
|
|
185
|
-
return
|
|
215
|
+
return (
|
|
216
|
+
active.target.provider === target.provider &&
|
|
217
|
+
active.target.id === target.id &&
|
|
218
|
+
active.thinkingLevel === thinkingLevel
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const candidate = { target, thinkingLevel };
|
|
222
|
+
if (this.#isNoop(candidate)) {
|
|
223
|
+
this.#disarmNoop(candidate);
|
|
224
|
+
return false;
|
|
186
225
|
}
|
|
187
|
-
this.#prewalk =
|
|
226
|
+
this.#prewalk = candidate;
|
|
188
227
|
this.#planInjected = true;
|
|
189
228
|
this.#continuePending = true;
|
|
229
|
+
this.#todoSeen = false;
|
|
190
230
|
this.#host.agent.steer({
|
|
191
231
|
role: "custom",
|
|
192
232
|
customType: PREWALK_PLAN_MESSAGE_TYPE,
|
|
@@ -200,6 +240,7 @@ export class PrewalkCoordinator {
|
|
|
200
240
|
`Prewalk: armed for ${target.provider}/${target.id} — will switch at the first edit/write once the todo list exists.`,
|
|
201
241
|
"prewalk",
|
|
202
242
|
);
|
|
243
|
+
return true;
|
|
203
244
|
}
|
|
204
245
|
|
|
205
246
|
/** Lazily enables plan-yolo's plan phase before the first prompt is built. */
|
|
@@ -220,8 +261,7 @@ export class PrewalkCoordinator {
|
|
|
220
261
|
|
|
221
262
|
#scrubPlanNudge(liveMessages: AgentMessage[]): void {
|
|
222
263
|
if (!this.#planInjected) return;
|
|
223
|
-
const isPlanNudge =
|
|
224
|
-
message.role === "custom" && message.customType === PREWALK_PLAN_MESSAGE_TYPE;
|
|
264
|
+
const isPlanNudge = isPrewalkPlanNudge;
|
|
225
265
|
for (let index = liveMessages.length - 1; index >= 0; index--) {
|
|
226
266
|
if (!isPlanNudge(liveMessages[index])) continue;
|
|
227
267
|
invalidateMessageCache(liveMessages[index]);
|