@oh-my-pi/pi-coding-agent 17.3.4 → 17.3.5
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 +68 -0
- package/dist/{CHANGELOG-trcc215s.md → CHANGELOG-tt9k4jpr.md} +68 -0
- package/dist/cli.js +3146 -3153
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/config/model-discovery.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +7 -2
- package/dist/types/extensibility/extensions/loader.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -3
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +22 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +2 -0
- package/dist/types/mcp/config.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +3 -1
- package/dist/types/registry/agent-lifecycle.d.ts +11 -0
- package/dist/types/registry/agent-registry.d.ts +4 -0
- package/dist/types/session/agent-session-types.d.ts +0 -2
- package/dist/types/session/agent-session.d.ts +20 -0
- package/dist/types/session/date-cwd-reminder.d.ts +22 -0
- package/dist/types/session/irc-bridge.d.ts +2 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-loader.d.ts +18 -11
- package/dist/types/session/session-maintenance.d.ts +6 -2
- package/dist/types/session/session-manager.d.ts +19 -3
- package/dist/types/session/session-tools.d.ts +0 -1
- package/dist/types/session/turn-recovery.d.ts +7 -7
- package/dist/types/tools/browser/attach.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +2 -2
- package/dist/types/tools/browser.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +6 -0
- package/dist/types/tools/hub/types.d.ts +6 -0
- package/dist/types/vibe/runtime.d.ts +3 -3
- package/package.json +13 -13
- package/src/auto-thinking/classifier.ts +37 -23
- package/src/cli/models-cli.ts +1 -1
- package/src/commit/analysis/conventional.ts +15 -9
- package/src/commit/analysis/summary.ts +15 -9
- package/src/commit/changelog/generate.ts +15 -9
- package/src/commit/map-reduce/map-phase.ts +3 -19
- package/src/commit/map-reduce/reduce-phase.ts +15 -9
- package/src/config/model-discovery.ts +3 -3
- package/src/config/model-registry.ts +29 -16
- package/src/config/settings-schema.ts +13 -0
- package/src/debug/report-bundle.ts +25 -59
- package/src/discovery/claude-plugins.ts +2 -1
- package/src/discovery/claude.ts +4 -2
- package/src/discovery/helpers.ts +5 -0
- package/src/edit/renderer.ts +48 -19
- package/src/extensibility/extensions/loader.ts +9 -5
- package/src/extensibility/extensions/runner.ts +124 -21
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/extensions/wrapper.ts +16 -10
- package/src/extensibility/legacy-pi-ai-shim.ts +4 -0
- package/src/extensibility/plugins/marketplace/manager.ts +15 -18
- package/src/irc/bus.ts +1 -1
- package/src/launch/broker.ts +8 -1
- package/src/launch/presence.ts +77 -1
- package/src/lsp/client.ts +35 -3
- package/src/lsp/clients/biome-client.ts +47 -93
- package/src/lsp/servers.ts +17 -11
- package/src/lsp/tool.ts +2 -4
- package/src/lsp/workspace-diagnostics.ts +24 -2
- package/src/mcp/config.ts +50 -6
- package/src/memories/index.ts +29 -25
- package/src/mnemopi/backend.ts +13 -11
- package/src/modes/components/ask-dialog.ts +25 -5
- package/src/modes/components/tool-execution.ts +10 -6
- package/src/modes/components/welcome.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +52 -32
- package/src/modes/controllers/selector-controller.ts +6 -0
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +49 -10
- package/src/modes/theme/defaults/birch.json +2 -2
- package/src/prompts/system/checkpoint-active-notice.md +5 -0
- package/src/prompts/system/date-cwd-reminder.md +3 -0
- package/src/prompts/system/project-prompt.md +0 -1
- package/src/prompts/system/rewind-report.md +1 -3
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/rewind.md +1 -13
- package/src/registry/agent-lifecycle.ts +34 -0
- package/src/registry/agent-registry.ts +27 -2
- package/src/registry/persisted-agents.ts +40 -20
- package/src/sdk.ts +32 -2
- package/src/session/agent-session-types.ts +0 -2
- package/src/session/agent-session.ts +178 -46
- package/src/session/date-cwd-reminder.ts +77 -0
- package/src/session/irc-bridge.ts +5 -0
- package/src/session/messages.ts +5 -1
- package/src/session/session-loader.ts +106 -64
- package/src/session/session-maintenance.ts +189 -115
- package/src/session/session-manager.ts +142 -35
- package/src/session/session-persistence.ts +4 -4
- package/src/session/session-storage.ts +18 -5
- package/src/session/session-tools.ts +5 -10
- package/src/session/turn-recovery.ts +102 -20
- package/src/session/unexpected-stop-classifier.ts +33 -21
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +4 -15
- package/src/task/persisted-revive.ts +3 -6
- package/src/tools/ask.ts +10 -3
- package/src/tools/browser/attach.ts +80 -25
- package/src/tools/browser/launch.ts +44 -15
- package/src/tools/browser/relay/daemon.ts +3 -7
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/shared-daemon.ts +3 -7
- package/src/tools/browser.ts +5 -5
- package/src/tools/builtin-names.ts +7 -3
- package/src/tools/checkpoint.ts +1 -7
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +20 -3
- package/src/tools/hub/types.ts +6 -0
- package/src/tools/render-utils.ts +53 -21
- package/src/tools/think.ts +15 -3
- package/src/tts/speech-enhancer.ts +19 -14
- package/src/utils/commit-message-generator.ts +14 -12
- package/src/utils/title-generator.ts +23 -19
- package/src/vibe/runtime.ts +32 -17
package/src/sdk.ts
CHANGED
|
@@ -136,6 +136,7 @@ import {
|
|
|
136
136
|
import { AgentSession, type InitialRetryFallbackState, type PlanYolo, type Prewalk } from "./session/agent-session";
|
|
137
137
|
import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config";
|
|
138
138
|
import type { AuthStorage } from "./session/auth-storage";
|
|
139
|
+
import { withDateCwdReminder } from "./session/date-cwd-reminder";
|
|
139
140
|
import { createInterruptedTurnAbortMessage } from "./session/exit-diagnostics";
|
|
140
141
|
import {
|
|
141
142
|
type CustomMessage,
|
|
@@ -222,6 +223,8 @@ import { ttsTool } from "./tools/tts";
|
|
|
222
223
|
import { resolveActiveRepoContext } from "./utils/active-repo-context";
|
|
223
224
|
import { EventBus } from "./utils/event-bus";
|
|
224
225
|
import { normalizeProviderContextImagesForModel } from "./utils/image-loading";
|
|
226
|
+
import { formatLocalCalendarDate } from "./utils/local-date";
|
|
227
|
+
import { normalizePromptPath } from "./utils/prompt-path";
|
|
225
228
|
import { buildNamedToolChoice } from "./utils/tool-choice";
|
|
226
229
|
import { VibeSessionRegistry } from "./vibe/runtime";
|
|
227
230
|
import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
@@ -3034,6 +3037,20 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
3034
3037
|
options.expectedAgentRef === undefined
|
|
3035
3038
|
? agentRegistry.register(registrationInput)
|
|
3036
3039
|
: agentRegistry.registerIfAvailable(registrationInput, options.expectedAgentRef);
|
|
3040
|
+
if (!registeredAgentRef && options.expectedAgentRef === null) {
|
|
3041
|
+
// A fresh spawn collided with an existing id. If that id is held by a
|
|
3042
|
+
// provably-dead parked corpse — no live session, no reviver — reclaim it
|
|
3043
|
+
// so this new generation can take the id instead of failing forever at
|
|
3044
|
+
// construction. Without this, one such corpse (isolated-run park,
|
|
3045
|
+
// interrupted construction) poisons the id for the whole process (#8490).
|
|
3046
|
+
// The reclaim is gated by the lifecycle owner and only touches the
|
|
3047
|
+
// registry it manages; the corpse's transcript stays at history://.
|
|
3048
|
+
const stale = agentRegistry.get(resolvedAgentId);
|
|
3049
|
+
const lifecycle = AgentLifecycleManager.global();
|
|
3050
|
+
if (stale && lifecycle.manages(agentRegistry) && (await lifecycle.reclaimDeadCorpse(resolvedAgentId, stale))) {
|
|
3051
|
+
registeredAgentRef = agentRegistry.registerIfAvailable(registrationInput, null);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3037
3054
|
if (!registeredAgentRef) {
|
|
3038
3055
|
throw new Error(`Agent "${resolvedAgentId}" is already owned by another session generation.`);
|
|
3039
3056
|
}
|
|
@@ -3130,7 +3147,15 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
3130
3147
|
let transformed = obfuscator ? obfuscateProviderContext(obfuscator, context) : context;
|
|
3131
3148
|
if (snapcompactInline) transformed = await snapcompactInline.transform(transformed, transformModel);
|
|
3132
3149
|
transformed = clampProviderContextImages(transformed, transformModel);
|
|
3133
|
-
|
|
3150
|
+
transformed = await normalizeProviderContextImagesForModel(transformed, transformModel);
|
|
3151
|
+
// Keep per-request volatility out of the system prompt: the date/cwd
|
|
3152
|
+
// reminder rides on the first user turn so open-weight providers keep
|
|
3153
|
+
// their tool-schema prefix cache (#7404).
|
|
3154
|
+
return withDateCwdReminder(
|
|
3155
|
+
transformed,
|
|
3156
|
+
formatLocalCalendarDate(),
|
|
3157
|
+
normalizePromptPath(sessionManager.getCwd()),
|
|
3158
|
+
);
|
|
3134
3159
|
};
|
|
3135
3160
|
const onPayload = async (payload: unknown, model?: Model) => {
|
|
3136
3161
|
return await extensionRunner.emitBeforeProviderRequest(payload, model);
|
|
@@ -3771,7 +3796,12 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
3771
3796
|
transformProviderContext: async (context, transformModel) => {
|
|
3772
3797
|
let transformed = obfuscator ? obfuscateProviderContext(obfuscator, context) : context;
|
|
3773
3798
|
transformed = clampProviderContextImages(transformed, transformModel);
|
|
3774
|
-
|
|
3799
|
+
transformed = await normalizeProviderContextImagesForModel(transformed, transformModel);
|
|
3800
|
+
return withDateCwdReminder(
|
|
3801
|
+
transformed,
|
|
3802
|
+
formatLocalCalendarDate(),
|
|
3803
|
+
normalizePromptPath(sessionManager.getCwd()),
|
|
3804
|
+
);
|
|
3775
3805
|
},
|
|
3776
3806
|
thinkingBudgets: agent.thinkingBudgets,
|
|
3777
3807
|
temperature: agent.temperature,
|
|
@@ -211,8 +211,6 @@ export interface AgentSessionConfig {
|
|
|
211
211
|
toolNames: string[],
|
|
212
212
|
tools: Map<string, AgentTool>,
|
|
213
213
|
) => Promise<{ systemPrompt: string[]; xdevCatalogNames?: readonly string[] }>;
|
|
214
|
-
/** Local calendar date provider used by prompt-cache invalidation. */
|
|
215
|
-
getLocalCalendarDate?: () => string;
|
|
216
214
|
/** Tools mounted under `xd://`, for `/tools` display. */
|
|
217
215
|
getXdevToolEntries?: () => Array<{ name: string; summary: string }>;
|
|
218
216
|
/** `xd://` presentation state backed by the canonical tool map. */
|
|
@@ -161,6 +161,7 @@ import type { PlanModeState } from "../plan-mode/state";
|
|
|
161
161
|
import goalModeContextPrompt from "../prompts/goals/goal-mode-context.md" with { type: "text" };
|
|
162
162
|
import goalTodoContextPrompt from "../prompts/goals/goal-todo-context.md" with { type: "text" };
|
|
163
163
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
164
|
+
import checkpointActiveNoticeTemplate from "../prompts/system/checkpoint-active-notice.md" with { type: "text" };
|
|
164
165
|
import interruptedThinkingTemplate from "../prompts/system/interrupted-thinking.md" with { type: "text" };
|
|
165
166
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
166
167
|
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
|
@@ -285,6 +286,7 @@ import {
|
|
|
285
286
|
import {
|
|
286
287
|
type BashExecutionMessage,
|
|
287
288
|
buildReplanTitleContext,
|
|
289
|
+
CHECKPOINT_ACTIVE_REMINDER_TYPE,
|
|
288
290
|
type CustomMessage,
|
|
289
291
|
type CustomMessagePayload,
|
|
290
292
|
convertToLlm,
|
|
@@ -297,6 +299,7 @@ import {
|
|
|
297
299
|
type InterruptedThinkingDetails,
|
|
298
300
|
isEmptyErrorTurn,
|
|
299
301
|
isUserInterruptAbort,
|
|
302
|
+
isUserInvokedSkillPrompt,
|
|
300
303
|
logProviderTurnError,
|
|
301
304
|
normalizeCustomMessagePayload,
|
|
302
305
|
type PythonExecutionMessage,
|
|
@@ -328,7 +331,7 @@ import {
|
|
|
328
331
|
SessionMaintenance,
|
|
329
332
|
type SessionMaintenanceHost,
|
|
330
333
|
} from "./session-maintenance";
|
|
331
|
-
import { cleanupEmptyMoveSession, type SessionManager } from "./session-manager";
|
|
334
|
+
import { cleanupEmptyMoveSession, copySessionArtifacts, type SessionManager } from "./session-manager";
|
|
332
335
|
import { SessionMemory, type SessionMemoryHost } from "./session-memory";
|
|
333
336
|
import { buildSessionMetadata } from "./session-metadata";
|
|
334
337
|
import { SessionProviderBoundary, type SessionProviderBoundaryHost } from "./session-provider-boundary";
|
|
@@ -488,6 +491,7 @@ export class AgentSession {
|
|
|
488
491
|
/** Last (enable, providerId) tuple resolved by `#syncAppendOnlyContext` — used to skip no-op invalidations. */
|
|
489
492
|
#lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
|
|
490
493
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
494
|
+
#runStateListeners = new Set<(state: "running" | "idle") => void>();
|
|
491
495
|
#commandMetadataChangedListeners: CommandMetadataChangedListener[] = [];
|
|
492
496
|
#sessionChangeCallbacks = new Set<() => void>();
|
|
493
497
|
#observedSessionId: string | undefined;
|
|
@@ -597,6 +601,7 @@ export class AgentSession {
|
|
|
597
601
|
#usageFallbackConfirmer: UsageFallbackConfirmer | undefined;
|
|
598
602
|
#usagePreflightAbortControllers = new Set<AbortController>();
|
|
599
603
|
#queuedMessageDrainBlocked = false;
|
|
604
|
+
#modeExitDrainSuppressionDepth = 0;
|
|
600
605
|
#usagePreflightReadyForNextModelCall = false;
|
|
601
606
|
#usagePreflightReadyModel: Model | undefined;
|
|
602
607
|
#detachUsageBeforeQueueDequeue: (() => void) | undefined;
|
|
@@ -771,7 +776,9 @@ export class AgentSession {
|
|
|
771
776
|
* the agent responds to the peer. Skip only when a queued steer/follow-up will itself drive a
|
|
772
777
|
* resume turn whose aside poll already consumes these (no double-wake). */
|
|
773
778
|
#resumeStrandedIrcAsides(): void {
|
|
774
|
-
if (this.#isDisposed || this.isStreaming || !this.#irc.hasPending())
|
|
779
|
+
if (this.#modeExitDrainSuppressionDepth > 0 || this.#isDisposed || this.isStreaming || !this.#irc.hasPending()) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
775
782
|
if (this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages()) return;
|
|
776
783
|
const records = this.#irc.drainPending();
|
|
777
784
|
if (this.#planModeState?.enabled) {
|
|
@@ -800,6 +807,10 @@ export class AgentSession {
|
|
|
800
807
|
* because #canAutoContinueForFollowUp suppresses follow-up auto-resume while a user interrupt is
|
|
801
808
|
* in effect, even though the wake left a provider-valid tail. */
|
|
802
809
|
#wakeForIrc(records: CustomMessage[]): void {
|
|
810
|
+
if (this.#modeExitDrainSuppressionDepth > 0) {
|
|
811
|
+
this.#irc.deferWake(records);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
803
814
|
// Park only a *blocked* follow-up (one a user interrupt is intentionally holding); an
|
|
804
815
|
// already-resumable follow-up can ride the wake turn normally without reordering.
|
|
805
816
|
const parkedFollowUps =
|
|
@@ -1295,7 +1306,6 @@ export class AgentSession {
|
|
|
1295
1306
|
presentationPinnedToolNames: config.presentationPinnedToolNames,
|
|
1296
1307
|
ensureWriteRegistered: config.ensureWriteRegistered,
|
|
1297
1308
|
rebuildSystemPrompt: config.rebuildSystemPrompt,
|
|
1298
|
-
getLocalCalendarDate: config.getLocalCalendarDate,
|
|
1299
1309
|
getMcpServerInstructions: config.getMcpServerInstructions,
|
|
1300
1310
|
xdev: config.xdev,
|
|
1301
1311
|
setActiveToolNames: config.setActiveToolNames,
|
|
@@ -1384,7 +1394,7 @@ export class AgentSession {
|
|
|
1384
1394
|
// Pre-scheduling tool_call wiring: extension handlers run at arg-prep
|
|
1385
1395
|
// time so a block/revision lands before concurrency resolution,
|
|
1386
1396
|
// tool_execution_start, and the wrapper's approval gate.
|
|
1387
|
-
this.agent.beforeToolCall = ctx => this.#beforeToolCall(ctx);
|
|
1397
|
+
this.agent.beforeToolCall = (ctx, signal) => this.#beforeToolCall(ctx, signal);
|
|
1388
1398
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
1389
1399
|
this.#syncAgentSessionId();
|
|
1390
1400
|
this.#todo.syncFromBranch();
|
|
@@ -1518,7 +1528,7 @@ export class AgentSession {
|
|
|
1518
1528
|
planReferencePath: () => this.#planReferencePath,
|
|
1519
1529
|
nonMessageTokenSource: () => this,
|
|
1520
1530
|
memoryBackendSession: () => this,
|
|
1521
|
-
emitSessionEvent: event => this.#emitSessionEvent(event),
|
|
1531
|
+
emitSessionEvent: (event, options) => this.#emitSessionEvent(event, options),
|
|
1522
1532
|
emitNotice: (level, message, source) => this.emitNotice(level, message, source),
|
|
1523
1533
|
schedulePostPromptTask: (task, options) => this.#schedulePostPromptTask(task, options),
|
|
1524
1534
|
scheduleAgentContinue: options => this.#scheduleAgentContinue(options),
|
|
@@ -1968,6 +1978,18 @@ export class AgentSession {
|
|
|
1968
1978
|
}
|
|
1969
1979
|
}
|
|
1970
1980
|
|
|
1981
|
+
#emitRunState(state: "running" | "idle"): void {
|
|
1982
|
+
for (const listener of this.#runStateListeners) {
|
|
1983
|
+
try {
|
|
1984
|
+
listener(state);
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
logger.warn("AgentSession run-state listener threw", {
|
|
1987
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1971
1993
|
/**
|
|
1972
1994
|
* Emit a UI-only notice to the session. Surfaces in interactive mode as a
|
|
1973
1995
|
* `showWarning` / `showError` / `showStatus` line; non-interactive modes
|
|
@@ -2063,7 +2085,7 @@ export class AgentSession {
|
|
|
2063
2085
|
*/
|
|
2064
2086
|
#subscriberEmitGate: Promise<void> = Promise.resolve();
|
|
2065
2087
|
|
|
2066
|
-
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
2088
|
+
async #emitSessionEvent(event: AgentSessionEvent, options: { detachExtensions?: boolean } = {}): Promise<void> {
|
|
2067
2089
|
if (event.type === "message_update") {
|
|
2068
2090
|
this.#emit(event);
|
|
2069
2091
|
void this.#queueExtensionEvent(event);
|
|
@@ -2076,7 +2098,17 @@ export class AgentSession {
|
|
|
2076
2098
|
const { promise: gate, resolve: releaseGate } = Promise.withResolvers<void>();
|
|
2077
2099
|
this.#subscriberEmitGate = gate;
|
|
2078
2100
|
try {
|
|
2079
|
-
|
|
2101
|
+
const extensionEmit = this.#emitExtensionEvent(event);
|
|
2102
|
+
if (options.detachExtensions) {
|
|
2103
|
+
void extensionEmit.catch(error => {
|
|
2104
|
+
logger.warn("Detached session event extension emit failed", {
|
|
2105
|
+
type: event.type,
|
|
2106
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2107
|
+
});
|
|
2108
|
+
});
|
|
2109
|
+
} else {
|
|
2110
|
+
await extensionEmit;
|
|
2111
|
+
}
|
|
2080
2112
|
await previousGate;
|
|
2081
2113
|
// Hold the wire-level agent_end until in-flight prompts unwind. Subscribers
|
|
2082
2114
|
// (rpc-mode, ACP, Cursor) treat agent_end as the "session is idle" signal;
|
|
@@ -2341,6 +2373,34 @@ export class AgentSession {
|
|
|
2341
2373
|
}
|
|
2342
2374
|
}
|
|
2343
2375
|
|
|
2376
|
+
/**
|
|
2377
|
+
* Builds the transient checkpoint-active reminder for a successful
|
|
2378
|
+
* checkpoint tool result, or undefined otherwise. The reminder is queued as
|
|
2379
|
+
* steering synchronously in the message_end handler (before any await), so
|
|
2380
|
+
* the agent loop folds it into the next provider call and persists it through
|
|
2381
|
+
* its normal custom-message event. Because the entry sits after the checkpoint
|
|
2382
|
+
* entry, the rewind branch cut drops it from the active path.
|
|
2383
|
+
*/
|
|
2384
|
+
#checkpointActiveReminderFor(
|
|
2385
|
+
message: AgentMessage,
|
|
2386
|
+
): CustomMessage<{ goal?: string; startedAt?: string }> | undefined {
|
|
2387
|
+
if (message.role !== "toolResult" || message.isError) return undefined;
|
|
2388
|
+
const semanticResult = semanticToolResult(message.toolName, message);
|
|
2389
|
+
if (semanticResult?.toolName !== "checkpoint") return undefined;
|
|
2390
|
+
const details = isRecord(semanticResult.details) ? semanticResult.details : undefined;
|
|
2391
|
+
const goal = details ? stringProperty(details, "goal") : undefined;
|
|
2392
|
+
const startedAt = details ? stringProperty(details, "startedAt") : undefined;
|
|
2393
|
+
return {
|
|
2394
|
+
role: "custom",
|
|
2395
|
+
customType: CHECKPOINT_ACTIVE_REMINDER_TYPE,
|
|
2396
|
+
content: prompt.render(checkpointActiveNoticeTemplate),
|
|
2397
|
+
display: false,
|
|
2398
|
+
details: { goal, startedAt },
|
|
2399
|
+
attribution: "agent",
|
|
2400
|
+
timestamp: Date.now(),
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2344
2404
|
#persistMessageEnd(message: AgentMessage): void {
|
|
2345
2405
|
if (message.role === "hookMessage" || message.role === "custom") {
|
|
2346
2406
|
// Prewalk's plan nudge is a one-run steering instruction. Persisting it would
|
|
@@ -2438,6 +2498,7 @@ export class AgentSession {
|
|
|
2438
2498
|
// turn: state-based lookups take over again.
|
|
2439
2499
|
if (event.type === "agent_start") {
|
|
2440
2500
|
this.#prunedTerminalRefusal = undefined;
|
|
2501
|
+
this.#emitRunState("running");
|
|
2441
2502
|
}
|
|
2442
2503
|
// Step the mid-run todo counter synchronously, BEFORE any await in this
|
|
2443
2504
|
// handler. The agent loop's next-turn `getAsideMessages` poll can run
|
|
@@ -2498,6 +2559,35 @@ export class AgentSession {
|
|
|
2498
2559
|
this.agent.appendMessage(interruptedThinkingMessage);
|
|
2499
2560
|
}
|
|
2500
2561
|
|
|
2562
|
+
// message_end listeners are fire-and-forget, and the agent loop runs against
|
|
2563
|
+
// a context cloned at prompt start. Appending only to agent.state would not
|
|
2564
|
+
// reach the next provider call in this tool loop. Queue the reminder as
|
|
2565
|
+
// steering before any await so the loop drains it at the next step boundary;
|
|
2566
|
+
// its normal custom-message event persists it after the checkpoint entry,
|
|
2567
|
+
// allowing the rewind branch cut to drop it from the active path.
|
|
2568
|
+
const checkpointReminder =
|
|
2569
|
+
event.type === "message_end" && event.message.role === "toolResult"
|
|
2570
|
+
? this.#checkpointActiveReminderFor(event.message)
|
|
2571
|
+
: undefined;
|
|
2572
|
+
if (checkpointReminder) {
|
|
2573
|
+
// Set #checkpointState synchronously too: the reminder is now visible to
|
|
2574
|
+
// the very next provider call, so a model that immediately calls `rewind`
|
|
2575
|
+
// must find an active checkpoint in RewindTool.execute(). The entry id is
|
|
2576
|
+
// backfilled post-await (see the toolResult handler) once the checkpoint
|
|
2577
|
+
// toolResult entry is persisted; #applyRewind runs on a later rewind turn,
|
|
2578
|
+
// never before that backfill.
|
|
2579
|
+
this.#checkpointState = {
|
|
2580
|
+
checkpointMessageCount: this.agent.state.messages.length,
|
|
2581
|
+
checkpointEntryId: null,
|
|
2582
|
+
startedAt:
|
|
2583
|
+
(checkpointReminder.details && stringProperty(checkpointReminder.details, "startedAt")) ??
|
|
2584
|
+
new Date().toISOString(),
|
|
2585
|
+
};
|
|
2586
|
+
this.#pendingRewindReport = undefined;
|
|
2587
|
+
this.#lastCompletedRewind = undefined;
|
|
2588
|
+
this.agent.steer(checkpointReminder);
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2501
2591
|
const messageEndPersistence =
|
|
2502
2592
|
event.type === "message_end" ? this.#createMessageEndPersistenceSlot(event.message) : undefined;
|
|
2503
2593
|
|
|
@@ -2704,15 +2794,24 @@ export class AgentSession {
|
|
|
2704
2794
|
);
|
|
2705
2795
|
}
|
|
2706
2796
|
if (semanticResult?.toolName === "checkpoint" && !isError) {
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2797
|
+
// Backfill the checkpoint entry id now that the toolResult entry is
|
|
2798
|
+
// persisted. #checkpointState was set synchronously pre-await (with a
|
|
2799
|
+
// null entry id) so an immediate `rewind` still finds an active
|
|
2800
|
+
// checkpoint; locate the toolResult's own entry by identity since the
|
|
2801
|
+
// transient reminder entry follows it (last-entry would branch-cut the
|
|
2802
|
+
// reminder instead of leaving it on the active path).
|
|
2803
|
+
const entries = this.sessionManager.getEntries();
|
|
2804
|
+
let checkpointEntryId: string | null = null;
|
|
2805
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
2806
|
+
const entry = entries[i];
|
|
2807
|
+
if (entry.type === "message" && entry.message === event.message) {
|
|
2808
|
+
checkpointEntryId = entry.id;
|
|
2809
|
+
break;
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
if (this.#checkpointState) {
|
|
2813
|
+
this.#checkpointState.checkpointEntryId = checkpointEntryId;
|
|
2814
|
+
}
|
|
2716
2815
|
}
|
|
2717
2816
|
if (semanticResult?.toolName === "rewind" && !isError && this.#checkpointState) {
|
|
2718
2817
|
const detailReport = semanticDetails ? (stringProperty(semanticDetails, "report")?.trim() ?? "") : "";
|
|
@@ -2733,6 +2832,7 @@ export class AgentSession {
|
|
|
2733
2832
|
// maintenance can emit agent_end, so preserve the state at settle entry.
|
|
2734
2833
|
const ttsrAbortPendingAtAgentEnd = this.#ttsr.abortPending;
|
|
2735
2834
|
const emitAgentEndNotification = async (options?: { willContinue?: boolean }) => {
|
|
2835
|
+
this.#emitRunState("idle");
|
|
2736
2836
|
// Public agent_end is held out of the eager display pass and emitted
|
|
2737
2837
|
// here after maintenance routing, tagged isTerminal so subscribers can
|
|
2738
2838
|
// tell final settles from scheduled continuations.
|
|
@@ -3297,7 +3397,7 @@ export class AgentSession {
|
|
|
3297
3397
|
* emit a second event (nested xd:// device dispatches and direct non-loop
|
|
3298
3398
|
* execution still emit there).
|
|
3299
3399
|
*/
|
|
3300
|
-
async #beforeToolCall(ctx: BeforeToolCallContext): Promise<BeforeToolCallResult | undefined> {
|
|
3400
|
+
async #beforeToolCall(ctx: BeforeToolCallContext, signal?: AbortSignal): Promise<BeforeToolCallResult | undefined> {
|
|
3301
3401
|
const runner = this.#extensionRunner;
|
|
3302
3402
|
if (!runner?.hasHandlers("tool_call")) return undefined;
|
|
3303
3403
|
const metadata = ctx.toolCall.providerMetadata;
|
|
@@ -3315,12 +3415,15 @@ export class AgentSession {
|
|
|
3315
3415
|
? { actions: computer.actions, pendingSafetyChecks: computer.pendingSafetyChecks }
|
|
3316
3416
|
: ctx.args;
|
|
3317
3417
|
runner.markToolCallEmitted(ctx.toolCall.id, ctx.tool.name);
|
|
3318
|
-
const callResult = await runner.emitToolCall(
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3418
|
+
const callResult = await runner.emitToolCall(
|
|
3419
|
+
{
|
|
3420
|
+
type: "tool_call",
|
|
3421
|
+
toolName: ctx.tool.name,
|
|
3422
|
+
toolCallId: ctx.toolCall.id,
|
|
3423
|
+
input: normalizeToolEventInput(ctx.tool.name, resolveToolEventInput(ctx.tool, eventArgs)),
|
|
3424
|
+
},
|
|
3425
|
+
signal,
|
|
3426
|
+
);
|
|
3324
3427
|
if (callResult?.block) {
|
|
3325
3428
|
return { block: true, reason: callResult.reason || "Tool execution was blocked by an extension" };
|
|
3326
3429
|
}
|
|
@@ -3608,6 +3711,15 @@ export class AgentSession {
|
|
|
3608
3711
|
};
|
|
3609
3712
|
}
|
|
3610
3713
|
|
|
3714
|
+
/**
|
|
3715
|
+
* Observe authoritative run-state transitions before public `agent_end`
|
|
3716
|
+
* deferral, for lifecycle owners that must not remain stale while prompts unwind.
|
|
3717
|
+
*/
|
|
3718
|
+
subscribeRunState(listener: (state: "running" | "idle") => void): () => void {
|
|
3719
|
+
this.#runStateListeners.add(listener);
|
|
3720
|
+
return () => this.#runStateListeners.delete(listener);
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3611
3723
|
/** Register cleanup that runs when this AgentSession adopts a different session ID. */
|
|
3612
3724
|
registerSessionChangeCallback(callback: () => void): () => void {
|
|
3613
3725
|
this.#sessionChangeCallbacks.add(callback);
|
|
@@ -3970,6 +4082,7 @@ export class AgentSession {
|
|
|
3970
4082
|
this.#unsubscribeModelRoles = undefined;
|
|
3971
4083
|
}
|
|
3972
4084
|
this.#eventListeners = [];
|
|
4085
|
+
this.#runStateListeners.clear();
|
|
3973
4086
|
this.#sessionChangeCallbacks.clear();
|
|
3974
4087
|
|
|
3975
4088
|
// A dispose triggered mid-turn (Ctrl-C / timeout / hard-killed subagent)
|
|
@@ -5568,10 +5681,14 @@ export class AgentSession {
|
|
|
5568
5681
|
}
|
|
5569
5682
|
|
|
5570
5683
|
// Auto thinking: classify this real user turn and set the effective level
|
|
5571
|
-
// before the model request.
|
|
5572
|
-
//
|
|
5573
|
-
//
|
|
5574
|
-
|
|
5684
|
+
// before the model request. A user-invoked `/skill:<name>` arrives as a
|
|
5685
|
+
// user-attributed skill custom message whose expanded body is the task
|
|
5686
|
+
// prompt, so it counts as a user turn. Synthetic/tool-continuation turns
|
|
5687
|
+
// (developer roles), agent-originated or autoloaded skill injections, and
|
|
5688
|
+
// non-auto sessions are skipped. Never blocks the turn — failures fall
|
|
5689
|
+
// back to a concrete level inside the helper.
|
|
5690
|
+
const isUserTurn = message.role === "user" || (message.role === "custom" && isUserInvokedSkillPrompt(message));
|
|
5691
|
+
if (this.isAutoThinking && isUserTurn) {
|
|
5575
5692
|
await this.#models.applyAutoThinkingLevel(expandedText, generation);
|
|
5576
5693
|
if (this.#promptGeneration !== generation) {
|
|
5577
5694
|
return;
|
|
@@ -5836,6 +5953,33 @@ export class AgentSession {
|
|
|
5836
5953
|
this.#scheduleIdleQueueDrain();
|
|
5837
5954
|
}
|
|
5838
5955
|
|
|
5956
|
+
/**
|
|
5957
|
+
* Run a mode-exit `teardown` (abort the active turn, swap the toolset, clear
|
|
5958
|
+
* mode state) with queued-message auto-resume suppressed, then re-arm the
|
|
5959
|
+
* drain so a queued user turn resumes cleanly once the previous toolset is
|
|
5960
|
+
* back.
|
|
5961
|
+
*
|
|
5962
|
+
* `abort()`'s stranded-queue drain runs from its own `finally`; without this
|
|
5963
|
+
* guard a queued steer/follow-up behind the aborted turn would start a fresh
|
|
5964
|
+
* `agent.continue()` during the teardown's `await`s — while the exiting mode's
|
|
5965
|
+
* tools/context are still live — and then have those tools removed underneath
|
|
5966
|
+
* it, reintroducing the mode's stale-tool failure on the restarted turn
|
|
5967
|
+
* (issue #8326). Suppressing the drain across teardown guarantees the queued
|
|
5968
|
+
* turn resumes only after teardown, so it runs as a clean non-mode turn.
|
|
5969
|
+
*/
|
|
5970
|
+
async runModeExitTeardown(teardown: () => Promise<void>): Promise<void> {
|
|
5971
|
+
this.#modeExitDrainSuppressionDepth++;
|
|
5972
|
+
try {
|
|
5973
|
+
await teardown();
|
|
5974
|
+
} finally {
|
|
5975
|
+
this.#modeExitDrainSuppressionDepth--;
|
|
5976
|
+
if (this.#modeExitDrainSuppressionDepth === 0) {
|
|
5977
|
+
this.#scheduleIdleQueueDrain();
|
|
5978
|
+
this.#resumeStrandedIrcAsides();
|
|
5979
|
+
}
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5982
|
+
|
|
5839
5983
|
async #queueUserMessage(
|
|
5840
5984
|
text: string,
|
|
5841
5985
|
images: ImageContent[] | undefined,
|
|
@@ -5884,6 +6028,7 @@ export class AgentSession {
|
|
|
5884
6028
|
#scheduleQueuedMessageDrain(): void {
|
|
5885
6029
|
if (
|
|
5886
6030
|
this.#queuedMessageDrainScheduled ||
|
|
6031
|
+
this.#modeExitDrainSuppressionDepth > 0 ||
|
|
5887
6032
|
this.#queuedMessageDrainBlocked ||
|
|
5888
6033
|
!this.#canAutoContinueForFollowUp() ||
|
|
5889
6034
|
!this.agent.hasQueuedMessages()
|
|
@@ -5894,7 +6039,11 @@ export class AgentSession {
|
|
|
5894
6039
|
this.#scheduleAgentContinue({
|
|
5895
6040
|
shouldContinue: () => {
|
|
5896
6041
|
this.#queuedMessageDrainScheduled = false;
|
|
5897
|
-
return
|
|
6042
|
+
return (
|
|
6043
|
+
this.#modeExitDrainSuppressionDepth === 0 &&
|
|
6044
|
+
this.#canAutoContinueForFollowUp() &&
|
|
6045
|
+
this.agent.hasQueuedMessages()
|
|
6046
|
+
);
|
|
5898
6047
|
},
|
|
5899
6048
|
onSkip: () => {
|
|
5900
6049
|
this.#queuedMessageDrainScheduled = false;
|
|
@@ -6701,24 +6850,7 @@ export class AgentSession {
|
|
|
6701
6850
|
// under a fresh id, so the work already produced is still this session's.
|
|
6702
6851
|
this.#recovery.reanchorServedAttribution(previousSessionId);
|
|
6703
6852
|
|
|
6704
|
-
|
|
6705
|
-
const oldArtifactDir = forkResult.oldSessionFile.slice(0, -6);
|
|
6706
|
-
const newArtifactDir = forkResult.newSessionFile.slice(0, -6);
|
|
6707
|
-
|
|
6708
|
-
try {
|
|
6709
|
-
const oldDirStat = await fs.promises.stat(oldArtifactDir);
|
|
6710
|
-
if (oldDirStat.isDirectory()) {
|
|
6711
|
-
await fs.promises.cp(oldArtifactDir, newArtifactDir, { recursive: true });
|
|
6712
|
-
}
|
|
6713
|
-
} catch (err) {
|
|
6714
|
-
if (!isEnoent(err)) {
|
|
6715
|
-
logger.warn("Failed to copy artifacts during fork", {
|
|
6716
|
-
oldArtifactDir,
|
|
6717
|
-
newArtifactDir,
|
|
6718
|
-
error: err instanceof Error ? err.message : String(err),
|
|
6719
|
-
});
|
|
6720
|
-
}
|
|
6721
|
-
}
|
|
6853
|
+
await copySessionArtifacts(forkResult.oldSessionFile, forkResult.newSessionFile);
|
|
6722
6854
|
|
|
6723
6855
|
// Update agent session ID
|
|
6724
6856
|
this.#freshProviderSessionId = undefined;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date/cwd reminder injection.
|
|
3
|
+
*
|
|
4
|
+
* The system prompt must stay byte-stable so open-weight chat templates that
|
|
5
|
+
* render tool schemas *after* the system content keep their prefix cache
|
|
6
|
+
* (#7404). The per-request date/cwd line used to live at the tail of the
|
|
7
|
+
* system prompt (`project-prompt.md`), which invalidated the whole tool array
|
|
8
|
+
* on every directory change or day rollover. It now rides on the first user
|
|
9
|
+
* turn of each provider request instead: built at request time (never stored
|
|
10
|
+
* in the session), deterministic per `(date, cwd)`, so the bytes are stable
|
|
11
|
+
* for the lifetime of a session/day and refresh automatically at midnight.
|
|
12
|
+
*/
|
|
13
|
+
import type { Context, Message } from "@oh-my-pi/pi-ai";
|
|
14
|
+
import { prompt } from "@oh-my-pi/pi-utils";
|
|
15
|
+
import dateCwdReminderTemplate from "../prompts/system/date-cwd-reminder.md" with { type: "text" };
|
|
16
|
+
|
|
17
|
+
/** Renders the reminder text for the given local calendar date and cwd. */
|
|
18
|
+
export function renderDateCwdReminder(date: string, cwd: string): string {
|
|
19
|
+
return prompt.render(dateCwdReminderTemplate, { date, cwd }).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Prepends `reminder` to the content of the first user message in `messages`,
|
|
24
|
+
* returning a new array. The input is never mutated. Returns the input
|
|
25
|
+
* unchanged when there is no user message to attach to, when the first user
|
|
26
|
+
* message already carries the exact reminder, or when an identical input was
|
|
27
|
+
* injected before with the same reminder.
|
|
28
|
+
*
|
|
29
|
+
* The memo is required by the append-only context path: it syncs the converted
|
|
30
|
+
* message objects into its log and reuses them across requests, and callers
|
|
31
|
+
* assert that identity is preserved for the stable prefix. Re-injecting the
|
|
32
|
+
* same pristine first user message with the same reminder must hand back the
|
|
33
|
+
* same injected message object, not a fresh clone. Keyed on the pristine
|
|
34
|
+
* message object (the append-only log hands back fresh array copies every
|
|
35
|
+
* turn, so array identity is not stable), with entries garbage-collected
|
|
36
|
+
* alongside the messages they belong to.
|
|
37
|
+
*/
|
|
38
|
+
const injectCache = new WeakMap<Message, { reminder: string; injected: Message }>();
|
|
39
|
+
|
|
40
|
+
export function injectDateCwdReminder(messages: Message[], reminder: string): Message[] {
|
|
41
|
+
const index = messages.findIndex(message => message.role === "user");
|
|
42
|
+
if (index < 0) return messages;
|
|
43
|
+
const first = messages[index]!;
|
|
44
|
+
if (typeof first.content === "string") {
|
|
45
|
+
if (first.content.startsWith(reminder)) return messages;
|
|
46
|
+
} else if (first.content[0]?.type === "text" && first.content[0].text === reminder) {
|
|
47
|
+
return messages;
|
|
48
|
+
}
|
|
49
|
+
const cached = injectCache.get(first);
|
|
50
|
+
if (cached !== undefined && cached.reminder === reminder) {
|
|
51
|
+
const out = messages.slice();
|
|
52
|
+
out[index] = cached.injected;
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
const content =
|
|
56
|
+
typeof first.content === "string"
|
|
57
|
+
? `${reminder}\n\n${first.content}`
|
|
58
|
+
: ([{ type: "text", text: reminder }, ...first.content] as Message["content"]);
|
|
59
|
+
const injected = { ...first, content } as Message;
|
|
60
|
+
injectCache.set(first, { reminder, injected });
|
|
61
|
+
const out = messages.slice();
|
|
62
|
+
out[index] = injected;
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Applies the date/cwd reminder to a provider `Context`, keeping the system
|
|
68
|
+
* prompt byte-stable for prompt caching. Skips NULL_PROMPT-style contexts
|
|
69
|
+
* (empty system prompt) so a no-prompt session stays byte-for-byte unchanged.
|
|
70
|
+
*/
|
|
71
|
+
export function withDateCwdReminder(context: Context, date: string, cwd: string): Context {
|
|
72
|
+
if (!context.systemPrompt || context.systemPrompt.length === 0) return context;
|
|
73
|
+
if (context.messages.length === 0) return context;
|
|
74
|
+
const reminder = renderDateCwdReminder(date, cwd);
|
|
75
|
+
const messages = injectDateCwdReminder(context.messages, reminder);
|
|
76
|
+
return messages === context.messages ? context : { ...context, messages };
|
|
77
|
+
}
|
|
@@ -51,6 +51,11 @@ export class IrcBridge {
|
|
|
51
51
|
return records;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Queues records whose idle wake must wait for a session transition to finish. */
|
|
55
|
+
deferWake(records: CustomMessage[]): void {
|
|
56
|
+
this.#asides.push(...records);
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
/** Surfaces and consumes queued incoming records before automatic injection. */
|
|
55
60
|
drainInboxMessages(agentId: string, opts?: { from?: string; limit?: number }): IrcMessage[] {
|
|
56
61
|
const messages: IrcMessage[] = [];
|
package/src/session/messages.ts
CHANGED
|
@@ -328,6 +328,9 @@ export type NormalizedCustomMessagePayload<T = unknown> = Pick<
|
|
|
328
328
|
/** Custom message type for hidden interrupted-thinking continuity context. */
|
|
329
329
|
export const INTERRUPTED_THINKING_MESSAGE_TYPE = "interrupted-thinking";
|
|
330
330
|
|
|
331
|
+
/** Custom message type for the transient checkpoint-active reminder. */
|
|
332
|
+
export const CHECKPOINT_ACTIVE_REMINDER_TYPE = "checkpoint-active-reminder";
|
|
333
|
+
|
|
331
334
|
/** Metadata persisted with a hidden interrupted-thinking continuity message. */
|
|
332
335
|
export interface InterruptedThinkingDetails {
|
|
333
336
|
interruptedAt: number;
|
|
@@ -1085,7 +1088,8 @@ function customMessageContentToLlmContent(content: CustomMessage["content"]): (T
|
|
|
1085
1088
|
return typeof content === "string" ? [{ type: "text", text: content }] : content;
|
|
1086
1089
|
}
|
|
1087
1090
|
|
|
1088
|
-
|
|
1091
|
+
/** True for a `/skill:<name>` prompt the user invoked directly (attribution `user`), as opposed to an agent/autoload injection. */
|
|
1092
|
+
export function isUserInvokedSkillPrompt(message: CustomMessage): boolean {
|
|
1089
1093
|
return message.customType === SKILL_PROMPT_MESSAGE_TYPE && message.attribution === "user";
|
|
1090
1094
|
}
|
|
1091
1095
|
|