@oh-my-pi/pi-coding-agent 17.2.5 → 17.2.6
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 +18 -0
- package/dist/{CHANGELOG-q2w43aw3.md → CHANGELOG-gs76k6wc.md} +18 -0
- package/dist/cli.js +3378 -3333
- package/dist/types/cli/gc-cli.d.ts +1 -0
- package/dist/types/launch/client.d.ts +9 -1
- package/dist/types/launch/protocol.d.ts +17 -0
- package/dist/types/modes/components/btw-panel.d.ts +3 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/security/contracts/schemas.d.ts +405 -403
- package/dist/types/session/agent-session-types.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +26 -2
- package/dist/types/session/launch-completion.d.ts +10 -0
- package/dist/types/session/session-entries.d.ts +15 -1
- package/dist/types/session/session-manager.d.ts +7 -0
- package/dist/types/session/yield-queue.d.ts +6 -1
- package/dist/types/tools/index.d.ts +9 -0
- package/package.json +12 -12
- package/src/cli/gc-cli.ts +641 -21
- package/src/config/model-discovery.ts +14 -14
- package/src/config/model-registry.ts +10 -8
- package/src/config/settings.ts +1 -1
- package/src/export/html/index.ts +10 -3
- package/src/export/share.ts +4 -0
- package/src/launch/broker.ts +222 -6
- package/src/launch/client.ts +161 -9
- package/src/launch/protocol.ts +52 -0
- package/src/main.ts +13 -6
- package/src/mcp/config-writer.ts +1 -1
- package/src/modes/components/btw-panel.ts +41 -4
- package/src/modes/controllers/btw-controller.ts +55 -7
- package/src/modes/controllers/command-controller.ts +31 -0
- package/src/modes/controllers/input-controller.ts +24 -7
- package/src/modes/interactive-mode.ts +16 -2
- package/src/modes/types.ts +8 -1
- package/src/prompts/session/launch-completion.md +1 -0
- package/src/sdk.ts +15 -0
- package/src/security/contracts/schemas.ts +205 -183
- package/src/security/contracts/validation.ts +5 -1
- package/src/security/store.ts +2 -2
- package/src/session/agent-session-types.ts +6 -0
- package/src/session/agent-session.ts +191 -14
- package/src/session/launch-completion.ts +37 -0
- package/src/session/session-context.ts +26 -0
- package/src/session/session-entries.ts +17 -1
- package/src/session/session-manager.ts +21 -0
- package/src/session/yield-queue.ts +121 -16
- package/src/slash-commands/builtin-registry.ts +10 -0
- package/src/tools/browser/cmux/cmux-tab.ts +92 -4
- package/src/tools/hub/launch.ts +122 -6
- package/src/tools/index.ts +9 -0
- package/dist/types/config/file-lock.d.ts +0 -29
- package/src/config/file-lock.ts +0 -164
|
@@ -144,6 +144,7 @@ import type { GoalModeState } from "../goals/state";
|
|
|
144
144
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
145
145
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
146
146
|
import type { IrcMessage } from "../irc/bus";
|
|
147
|
+
import type { DaemonCompletionNotification } from "../launch/protocol";
|
|
147
148
|
import { shutdownMnemopiEmbedClient } from "../mnemopi/embed-client";
|
|
148
149
|
import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
|
|
149
150
|
import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
@@ -221,6 +222,7 @@ import type {
|
|
|
221
222
|
ModelCycleResult,
|
|
222
223
|
Prewalk,
|
|
223
224
|
PromptOptions,
|
|
225
|
+
ResetSessionContextResult,
|
|
224
226
|
ResolvedRoleModel,
|
|
225
227
|
RestoredQueuedMessage,
|
|
226
228
|
RoleModelCycle,
|
|
@@ -271,6 +273,12 @@ import {
|
|
|
271
273
|
type ToolExecutionStartData,
|
|
272
274
|
} from "./exit-diagnostics";
|
|
273
275
|
import { IrcBridge, type IrcBridgeHost } from "./irc-bridge";
|
|
276
|
+
import {
|
|
277
|
+
buildLaunchCompletionBatchMessage,
|
|
278
|
+
isLaunchCompletionOwner,
|
|
279
|
+
LAUNCH_COMPLETION_MESSAGE_TYPE,
|
|
280
|
+
type LaunchCompletionEntry,
|
|
281
|
+
} from "./launch-completion";
|
|
274
282
|
import {
|
|
275
283
|
type BashExecutionMessage,
|
|
276
284
|
buildReplanTitleContext,
|
|
@@ -340,6 +348,7 @@ import { TodoTracker, type TodoTrackerHost } from "./todo-tracker";
|
|
|
340
348
|
import { TtsrCoordinator, type TtsrCoordinatorHost } from "./ttsr-coordinator";
|
|
341
349
|
|
|
342
350
|
const PLAN_MODE_REMINDER_MAX = 3;
|
|
351
|
+
const POST_PROMPT_DRAIN_TIMEOUT_MS = 5_000;
|
|
343
352
|
|
|
344
353
|
/** Internal marker for hook messages queued through the agent loop */
|
|
345
354
|
// ============================================================================
|
|
@@ -445,6 +454,8 @@ export class AgentSession {
|
|
|
445
454
|
#lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
|
|
446
455
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
447
456
|
#commandMetadataChangedListeners: CommandMetadataChangedListener[] = [];
|
|
457
|
+
#sessionChangeCallbacks = new Set<() => void>();
|
|
458
|
+
#observedSessionId: string | undefined;
|
|
448
459
|
|
|
449
460
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
450
461
|
#pendingNextTurnMessages: CustomMessage[] = [];
|
|
@@ -658,6 +669,7 @@ export class AgentSession {
|
|
|
658
669
|
if (onSettled) this.#inFlightSettledCallbacks.push(onSettled);
|
|
659
670
|
this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
|
|
660
671
|
if (this.#promptInFlightCount !== 0) return;
|
|
672
|
+
this.yieldQueue.requestIdleFlush();
|
|
661
673
|
this.#releasePowerAssertion();
|
|
662
674
|
this.#flushPendingAgentEnd();
|
|
663
675
|
if (this.#inFlightSettledCallbacks.length === 0) {
|
|
@@ -839,6 +851,7 @@ export class AgentSession {
|
|
|
839
851
|
|
|
840
852
|
#resetInFlight(): void {
|
|
841
853
|
this.#promptInFlightCount = 0;
|
|
854
|
+
this.yieldQueue.requestIdleFlush();
|
|
842
855
|
this.#releasePowerAssertion();
|
|
843
856
|
this.#flushPendingAgentEnd();
|
|
844
857
|
if (this.#inFlightSettledCallbacks.length === 0) {
|
|
@@ -1122,17 +1135,30 @@ export class AgentSession {
|
|
|
1122
1135
|
injectIdle: async messages => {
|
|
1123
1136
|
const first = messages[0];
|
|
1124
1137
|
if (!first) return;
|
|
1125
|
-
|
|
1138
|
+
this.#beginInFlight();
|
|
1139
|
+
try {
|
|
1140
|
+
await this.agent.prompt(messages.length === 1 ? first : messages);
|
|
1141
|
+
} finally {
|
|
1142
|
+
this.#endInFlight();
|
|
1143
|
+
}
|
|
1126
1144
|
},
|
|
1127
1145
|
scheduleIdleFlush: run => {
|
|
1128
1146
|
this.#schedulePostPromptTask(
|
|
1129
1147
|
async () => {
|
|
1130
1148
|
await run();
|
|
1131
1149
|
},
|
|
1132
|
-
{
|
|
1150
|
+
{
|
|
1151
|
+
delayMs: 1,
|
|
1152
|
+
onSkip: () => this.yieldQueue.cancelIdleFlushScheduling(),
|
|
1153
|
+
},
|
|
1133
1154
|
);
|
|
1134
1155
|
},
|
|
1135
1156
|
});
|
|
1157
|
+
this.yieldQueue.register<LaunchCompletionEntry>(LAUNCH_COMPLETION_MESSAGE_TYPE, {
|
|
1158
|
+
isStale: entry =>
|
|
1159
|
+
this.#isDisposed || !isLaunchCompletionOwner(entry.owner, this.sessionManager.getSessionId()),
|
|
1160
|
+
build: buildLaunchCompletionBatchMessage,
|
|
1161
|
+
});
|
|
1136
1162
|
// Background-job completions / late diagnostics are pulled into the run at
|
|
1137
1163
|
// each step boundary as non-interrupting asides. Peer IRCs share the aside
|
|
1138
1164
|
// injection boundary, but also expose a non-consuming interrupt peek so
|
|
@@ -2900,6 +2926,7 @@ export class AgentSession {
|
|
|
2900
2926
|
try {
|
|
2901
2927
|
await scheduler.wait(delayMs, { signal });
|
|
2902
2928
|
} catch {
|
|
2929
|
+
if (signal.aborted) options?.onSkip?.("aborted");
|
|
2903
2930
|
return;
|
|
2904
2931
|
}
|
|
2905
2932
|
}
|
|
@@ -3400,6 +3427,12 @@ export class AgentSession {
|
|
|
3400
3427
|
};
|
|
3401
3428
|
}
|
|
3402
3429
|
|
|
3430
|
+
/** Register cleanup that runs when this AgentSession adopts a different session ID. */
|
|
3431
|
+
registerSessionChangeCallback(callback: () => void): () => void {
|
|
3432
|
+
this.#sessionChangeCallbacks.add(callback);
|
|
3433
|
+
return () => this.#sessionChangeCallbacks.delete(callback);
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3403
3436
|
subscribeCommandMetadataChanged(listener: CommandMetadataChangedListener): () => void {
|
|
3404
3437
|
this.#commandMetadataChangedListeners.push(listener);
|
|
3405
3438
|
return () => {
|
|
@@ -3474,7 +3507,14 @@ export class AgentSession {
|
|
|
3474
3507
|
* (login/logout, token refresh that surfaces a new account UUID) without
|
|
3475
3508
|
* needing to re-call `#syncAgentSessionId()` on every such event.
|
|
3476
3509
|
*/
|
|
3477
|
-
#syncAgentSessionId(sessionId?: string): void {
|
|
3510
|
+
#syncAgentSessionId(sessionId?: string, notifyChange = true): void {
|
|
3511
|
+
const currentSessionId = this.sessionManager.getSessionId();
|
|
3512
|
+
if (this.#observedSessionId === undefined) {
|
|
3513
|
+
this.#observedSessionId = currentSessionId;
|
|
3514
|
+
} else if (this.#observedSessionId !== currentSessionId) {
|
|
3515
|
+
this.#observedSessionId = currentSessionId;
|
|
3516
|
+
if (notifyChange) this.#notifySessionChangeCallbacks();
|
|
3517
|
+
}
|
|
3478
3518
|
const sid = this.#activeProviderSessionId(sessionId);
|
|
3479
3519
|
this.agent.sessionId = sid;
|
|
3480
3520
|
this.agent.setMetadataResolver((provider: string) =>
|
|
@@ -3496,6 +3536,16 @@ export class AgentSession {
|
|
|
3496
3536
|
if (this.#advisors) this.#advisors.refreshProviderIdentity();
|
|
3497
3537
|
}
|
|
3498
3538
|
|
|
3539
|
+
#notifySessionChangeCallbacks(): void {
|
|
3540
|
+
for (const callback of [...this.#sessionChangeCallbacks]) {
|
|
3541
|
+
try {
|
|
3542
|
+
callback();
|
|
3543
|
+
} catch (error) {
|
|
3544
|
+
logger.warn("Session change callback failed", { error: String(error) });
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3499
3549
|
/** Run one abortable auto-learn capture outside the primary agent loop. */
|
|
3500
3550
|
async runAutolearnCapture(capture: (signal: AbortSignal) => Promise<void>): Promise<void> {
|
|
3501
3551
|
if (this.#autolearnCaptureTask || this.#isDisposed) return;
|
|
@@ -3676,7 +3726,11 @@ export class AgentSession {
|
|
|
3676
3726
|
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
3677
3727
|
this.agent.abort();
|
|
3678
3728
|
try {
|
|
3679
|
-
await withTimeout(
|
|
3729
|
+
await withTimeout(
|
|
3730
|
+
postPromptDrain,
|
|
3731
|
+
POST_PROMPT_DRAIN_TIMEOUT_MS,
|
|
3732
|
+
"Timed out draining post-prompt tasks during dispose",
|
|
3733
|
+
);
|
|
3680
3734
|
} catch (error) {
|
|
3681
3735
|
logger.warn("Post-prompt tasks still draining at dispose deadline", { error: String(error) });
|
|
3682
3736
|
}
|
|
@@ -3723,6 +3777,7 @@ export class AgentSession {
|
|
|
3723
3777
|
this.#unsubscribeModelRoles = undefined;
|
|
3724
3778
|
}
|
|
3725
3779
|
this.#eventListeners = [];
|
|
3780
|
+
this.#sessionChangeCallbacks.clear();
|
|
3726
3781
|
}
|
|
3727
3782
|
|
|
3728
3783
|
#closeAllProviderSessions(reason: string): void {
|
|
@@ -3757,6 +3812,101 @@ export class AgentSession {
|
|
|
3757
3812
|
};
|
|
3758
3813
|
}
|
|
3759
3814
|
|
|
3815
|
+
/**
|
|
3816
|
+
* Reset the current conversation in place: drop every message, queued turn,
|
|
3817
|
+
* and pending tool call from the model's context while keeping the session
|
|
3818
|
+
* itself — its id, title, cwd, model, settings, and on-disk transcript all
|
|
3819
|
+
* survive. The next turn is sent with only the base system prompt plus the
|
|
3820
|
+
* project rules/AGENTS.md.
|
|
3821
|
+
*
|
|
3822
|
+
* This is the in-place sibling of {@link newSession}: it reuses the same
|
|
3823
|
+
* conversation-boundary teardown (drop the conversation, rotate provider-side
|
|
3824
|
+
* session state so providers that keep history server-side resume nothing,
|
|
3825
|
+
* re-prime the advisors, and undo any memory promotion) but skips minting a
|
|
3826
|
+
* new session id and opening a fresh transcript file. Unlike
|
|
3827
|
+
* {@link freshSession} (which only rotates provider stream state) it also
|
|
3828
|
+
* clears the conversation.
|
|
3829
|
+
*
|
|
3830
|
+
* Returns `undefined` without mutating anything while a response is
|
|
3831
|
+
* streaming or a foreground bash/python execution is in flight.
|
|
3832
|
+
*/
|
|
3833
|
+
async resetSessionContext(): Promise<ResetSessionContextResult | undefined> {
|
|
3834
|
+
// Refuse while a response streams OR a foreground user bash/python
|
|
3835
|
+
// execution is in flight: those complete via recordBashResult()/
|
|
3836
|
+
// recordPythonResult(), which append directly to agent.state when not
|
|
3837
|
+
// streaming, so a command finishing after the reset would land its output
|
|
3838
|
+
// after the boundary and re-enter the supposedly empty context. The
|
|
3839
|
+
// sibling boundary op (branchFromBtw) guards on the same predicates.
|
|
3840
|
+
if (this.isStreaming || this.isBashRunning || this.isEvalRunning) return undefined;
|
|
3841
|
+
const droppedCount = this.agent.state.messages.length;
|
|
3842
|
+
|
|
3843
|
+
// Tear down the same per-turn runtime state that newSession() resets across
|
|
3844
|
+
// a conversation boundary, so work scheduled from the pre-reset turn cannot
|
|
3845
|
+
// re-enter the cleared context:
|
|
3846
|
+
// - bump #promptGeneration + drain post-prompt tasks so an already-queued
|
|
3847
|
+
// post-prompt continuation (recovery can be scheduled after agent_end
|
|
3848
|
+
// while isStreaming is false) sees a stale generation and skips
|
|
3849
|
+
// (mirrors abort()).
|
|
3850
|
+
// - cancel this agent's async bash/task jobs so their completions can't
|
|
3851
|
+
// re-deliver stale tool output into the cleared conversation
|
|
3852
|
+
// (mirrors newSession()).
|
|
3853
|
+
this.#promptGeneration++;
|
|
3854
|
+
await this.#cancelPostPromptTasks();
|
|
3855
|
+
this.#cancelOwnAsyncJobs();
|
|
3856
|
+
|
|
3857
|
+
// Drop the conversation: messages, queued steers/follow-ups, pending tool
|
|
3858
|
+
// calls, and error state. agent.reset() keeps the model and system prompt.
|
|
3859
|
+
this.agent.reset();
|
|
3860
|
+
this.#pendingNextTurnMessages = [];
|
|
3861
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
3862
|
+
// Reset the session_stop continuation chain: the queued continuation
|
|
3863
|
+
// message is gone with the conversation, but the counters would otherwise
|
|
3864
|
+
// carry over, so the next post-reset turn is reported to hooks as part of
|
|
3865
|
+
// the old chain and can hit SESSION_STOP_CONTINUATION_CAP early (mirrors
|
|
3866
|
+
// abort()/newSession()).
|
|
3867
|
+
this.#resetSessionStopContinuationState();
|
|
3868
|
+
|
|
3869
|
+
// Drop checkpoint/rewind runtime state and deferred tool directives
|
|
3870
|
+
// alongside the messages that carried them: the checkpoint tool result is
|
|
3871
|
+
// gone from agent.state, so an intact #checkpointState would otherwise
|
|
3872
|
+
// force a rewind onto the pre-reset transcript on the next turn (mirrors
|
|
3873
|
+
// newSession()).
|
|
3874
|
+
this.#clearCheckpointRuntimeState();
|
|
3875
|
+
this.#clearSessionScopedToolState();
|
|
3876
|
+
|
|
3877
|
+
// Rotate provider-side session state so a provider that keeps conversation
|
|
3878
|
+
// history server-side starts a brand-new exchange rather than resuming the
|
|
3879
|
+
// context we just dropped (mirrors freshSession()).
|
|
3880
|
+
this.#closeAllProviderSessions("reset context");
|
|
3881
|
+
this.#freshProviderSessionId = Bun.randomUUIDv7();
|
|
3882
|
+
this.#syncAgentSessionId();
|
|
3883
|
+
this.#memory.rekeyForCurrentSessionId();
|
|
3884
|
+
this.agent.appendOnlyContext?.invalidateForModelChange();
|
|
3885
|
+
|
|
3886
|
+
// Re-arm the approved-plan reference: the reset dropped the plan-approved
|
|
3887
|
+
// prompt/reference from agent.state, so mark it unsent (preserving the
|
|
3888
|
+
// path — the plan file on disk is still the active plan) to let
|
|
3889
|
+
// #buildPlanReferenceMessage re-read and re-inject it on the next turn.
|
|
3890
|
+
// Mirrors the sent-flag reset newSession() and compaction perform after a
|
|
3891
|
+
// history rewrite (issue #1246).
|
|
3892
|
+
this.#planReferenceSent = false;
|
|
3893
|
+
|
|
3894
|
+
// Re-prime the advisors across the conversation boundary and undo any
|
|
3895
|
+
// memory promotion so the next turn rebuilds from the base system prompt.
|
|
3896
|
+
this.#advisors.resetSessionState();
|
|
3897
|
+
await this.#memory.resetContextForNewTranscript();
|
|
3898
|
+
|
|
3899
|
+
// Record a durable boundary on the persisted branch. The collapsed live
|
|
3900
|
+
// transcript and the model-context rebuild start emission after the latest
|
|
3901
|
+
// boundary, so a rebuild across a `/reset` (theme change, focus attach,
|
|
3902
|
+
// /shake, resume) does not resurrect the pre-reset conversation. The
|
|
3903
|
+
// on-disk record and the plain `transcript:true` export path keep the full
|
|
3904
|
+
// pre-reset history.
|
|
3905
|
+
this.sessionManager.appendResetBoundary();
|
|
3906
|
+
|
|
3907
|
+
return { droppedCount };
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3760
3910
|
// =========================================================================
|
|
3761
3911
|
// Read-only State Access
|
|
3762
3912
|
// =========================================================================
|
|
@@ -5534,6 +5684,16 @@ export class AgentSession {
|
|
|
5534
5684
|
this.#queueHiddenNextTurnMessage(message, true);
|
|
5535
5685
|
}
|
|
5536
5686
|
|
|
5687
|
+
queueLaunchCompletion(notification: DaemonCompletionNotification): Promise<void> {
|
|
5688
|
+
if (this.#isDisposed) return Promise.reject(new Error("Session disposed before launch completion delivery"));
|
|
5689
|
+
const delivered = this.yieldQueue.enqueueWithReceipt<LaunchCompletionEntry>(
|
|
5690
|
+
LAUNCH_COMPLETION_MESSAGE_TYPE,
|
|
5691
|
+
notification,
|
|
5692
|
+
);
|
|
5693
|
+
this.yieldQueue.requestIdleFlush();
|
|
5694
|
+
return delivered;
|
|
5695
|
+
}
|
|
5696
|
+
|
|
5537
5697
|
#queueHiddenNextTurnMessage(message: CustomMessage, triggerTurn: boolean): void {
|
|
5538
5698
|
this.#pendingNextTurnMessages.push(message);
|
|
5539
5699
|
if (!triggerTurn) return;
|
|
@@ -7251,7 +7411,7 @@ export class AgentSession {
|
|
|
7251
7411
|
this.#clearInheritedProviderPromptCacheKey();
|
|
7252
7412
|
this.#adoptInheritedProviderPromptCacheKey();
|
|
7253
7413
|
}
|
|
7254
|
-
this.#syncAgentSessionId();
|
|
7414
|
+
this.#syncAgentSessionId(undefined, false);
|
|
7255
7415
|
this.#memory.rekeyForCurrentSessionId();
|
|
7256
7416
|
|
|
7257
7417
|
let sessionContext = this.buildDisplaySessionContext();
|
|
@@ -7388,11 +7548,14 @@ export class AgentSession {
|
|
|
7388
7548
|
this.#advisors.restoreCost(await loadAdvisorTranscriptCosts(this.sessionFile));
|
|
7389
7549
|
}
|
|
7390
7550
|
this.#bash.finishSessionTransition(bashTransition, true);
|
|
7551
|
+
if (previousSessionState.sessionId !== this.sessionManager.getSessionId()) {
|
|
7552
|
+
this.#notifySessionChangeCallbacks();
|
|
7553
|
+
}
|
|
7391
7554
|
return true;
|
|
7392
7555
|
} catch (error) {
|
|
7393
7556
|
this.sessionManager.restoreState(previousSessionState);
|
|
7394
7557
|
this.#freshProviderSessionId = previousFreshProviderSessionId;
|
|
7395
|
-
this.#syncAgentSessionId(previousSessionState.sessionId);
|
|
7558
|
+
this.#syncAgentSessionId(previousSessionState.sessionId, false);
|
|
7396
7559
|
this.#memory.rekeyForCurrentSessionId();
|
|
7397
7560
|
this.agent.setTools(previousTools);
|
|
7398
7561
|
this.#tools.setBaseSystemPrompt(previousBaseSystemPrompt);
|
|
@@ -7506,7 +7669,10 @@ export class AgentSession {
|
|
|
7506
7669
|
await this.#advisors.drainAndDetachRecorders();
|
|
7507
7670
|
try {
|
|
7508
7671
|
if (!selectedEntry.parentId) {
|
|
7672
|
+
const title = this.sessionManager.getSessionName();
|
|
7673
|
+
const titleSource = this.sessionManager.titleSource;
|
|
7509
7674
|
await this.sessionManager.newSession({ parentSession: previousSessionFile });
|
|
7675
|
+
if (title) await this.sessionManager.setSessionName(title, titleSource);
|
|
7510
7676
|
} else {
|
|
7511
7677
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
7512
7678
|
}
|
|
@@ -7553,21 +7719,24 @@ export class AgentSession {
|
|
|
7553
7719
|
}
|
|
7554
7720
|
}
|
|
7555
7721
|
|
|
7722
|
+
/** Promotes a completed /btw answer from the explicitly authorized session and leaf. */
|
|
7556
7723
|
async branchFromBtw(
|
|
7557
7724
|
question: string,
|
|
7558
7725
|
assistantMessage: AssistantMessage,
|
|
7726
|
+
leafId: string,
|
|
7727
|
+
sessionId: string,
|
|
7559
7728
|
): Promise<{ cancelled: boolean; sessionFile: string | undefined }> {
|
|
7560
7729
|
const previousSessionFile = this.sessionFile;
|
|
7561
7730
|
if (!this.sessionManager.getSessionFile()) {
|
|
7562
7731
|
throw new Error("Cannot branch /btw: session is not persisted");
|
|
7563
7732
|
}
|
|
7564
7733
|
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
throw new Error("Cannot branch /btw: current session has no leaf");
|
|
7734
|
+
if (!leafId || this.sessionManager.getSessionId() !== sessionId || this.sessionManager.getLeafId() !== leafId) {
|
|
7735
|
+
throw new Error("Cannot branch /btw: session changed since /btw started");
|
|
7568
7736
|
}
|
|
7569
7737
|
|
|
7570
7738
|
if (
|
|
7739
|
+
this.isStreaming ||
|
|
7571
7740
|
this.isBashRunning ||
|
|
7572
7741
|
this.isEvalRunning ||
|
|
7573
7742
|
this.isCompacting ||
|
|
@@ -7588,8 +7757,17 @@ export class AgentSession {
|
|
|
7588
7757
|
}
|
|
7589
7758
|
}
|
|
7590
7759
|
|
|
7591
|
-
|
|
7760
|
+
if (this.sessionManager.getSessionId() !== sessionId || this.sessionManager.getLeafId() !== leafId) {
|
|
7761
|
+
throw new Error("Cannot branch /btw: session changed since /btw started");
|
|
7762
|
+
}
|
|
7763
|
+
|
|
7764
|
+
await withTimeout(
|
|
7765
|
+
this.#cancelPostPromptTasks(),
|
|
7766
|
+
POST_PROMPT_DRAIN_TIMEOUT_MS,
|
|
7767
|
+
"Timed out draining post-prompt tasks before /btw branch",
|
|
7768
|
+
);
|
|
7592
7769
|
if (
|
|
7770
|
+
this.isStreaming ||
|
|
7593
7771
|
this.isBashRunning ||
|
|
7594
7772
|
this.isEvalRunning ||
|
|
7595
7773
|
this.isCompacting ||
|
|
@@ -7602,10 +7780,6 @@ export class AgentSession {
|
|
|
7602
7780
|
this.#pendingNextTurnMessages = [];
|
|
7603
7781
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
7604
7782
|
this.agent.replaceQueues([], []);
|
|
7605
|
-
if (this.isStreaming) {
|
|
7606
|
-
await this.abort({ goalReason: "internal", reason: "branching /btw" });
|
|
7607
|
-
this.agent.replaceQueues([], []);
|
|
7608
|
-
}
|
|
7609
7783
|
await this.#bash.flushPending();
|
|
7610
7784
|
await this.sessionManager.flush();
|
|
7611
7785
|
const bashTransition = this.#bash.beginSessionTransition();
|
|
@@ -7619,6 +7793,9 @@ export class AgentSession {
|
|
|
7619
7793
|
advisorRecordersDetached = true;
|
|
7620
7794
|
await this.#advisors.drainAndDetachRecorders();
|
|
7621
7795
|
try {
|
|
7796
|
+
if (this.sessionManager.getSessionId() !== sessionId || this.sessionManager.getLeafId() !== leafId) {
|
|
7797
|
+
throw new Error("Cannot branch /btw: session changed since /btw started");
|
|
7798
|
+
}
|
|
7622
7799
|
this.sessionManager.createBranchedSession(leafId);
|
|
7623
7800
|
this.#bash.markSessionTransition(bashTransition);
|
|
7624
7801
|
this.#advisors.clearCost();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { prompt } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import type { DaemonCompletionNotification } from "../launch/protocol";
|
|
3
|
+
import launchCompletionTemplate from "../prompts/session/launch-completion.md" with { type: "text" };
|
|
4
|
+
import type { CustomMessage } from "./messages";
|
|
5
|
+
|
|
6
|
+
/** Yield-queue kind for broker-owned supervised process completions. */
|
|
7
|
+
export const LAUNCH_COMPLETION_MESSAGE_TYPE = "launch-completion";
|
|
8
|
+
|
|
9
|
+
/** One broker completion awaiting injection into its owning session. */
|
|
10
|
+
export type LaunchCompletionEntry = DaemonCompletionNotification;
|
|
11
|
+
|
|
12
|
+
/** Whether a broker completion belongs to the primary session or its advisor. */
|
|
13
|
+
export function isLaunchCompletionOwner(owner: string, sessionId: string): boolean {
|
|
14
|
+
return owner === sessionId || owner === `${sessionId}-advisor`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Build one model-visible notification per terminal supervised process exit. */
|
|
18
|
+
export function buildLaunchCompletionBatchMessage(entries: LaunchCompletionEntry[]): CustomMessage {
|
|
19
|
+
return {
|
|
20
|
+
role: "custom",
|
|
21
|
+
customType: LAUNCH_COMPLETION_MESSAGE_TYPE,
|
|
22
|
+
content: entries
|
|
23
|
+
.map(({ daemon }) =>
|
|
24
|
+
prompt.render(launchCompletionTemplate, {
|
|
25
|
+
name: daemon.name,
|
|
26
|
+
state: daemon.state,
|
|
27
|
+
exitCode: daemon.exitCode,
|
|
28
|
+
hasExitCode: daemon.exitCode !== undefined,
|
|
29
|
+
}),
|
|
30
|
+
)
|
|
31
|
+
.join("\n"),
|
|
32
|
+
display: true,
|
|
33
|
+
attribution: "agent",
|
|
34
|
+
details: { daemons: entries.map(entry => entry.daemon) },
|
|
35
|
+
timestamp: Date.now(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -284,6 +284,12 @@ export function buildSessionContext(
|
|
|
284
284
|
|
|
285
285
|
const injectedTtsrRules = Array.from(injectedTtsrRulesSet);
|
|
286
286
|
|
|
287
|
+
// Index on the path of the latest `/reset` boundary, or -1 when none. The
|
|
288
|
+
// collapsed live transcript and the model-context rebuild start emission
|
|
289
|
+
// after it (see the emission branch below); the full-history export path
|
|
290
|
+
// ignores it.
|
|
291
|
+
const resetBoundaryIdx = path.reduce((latest, entry, i) => (entry.type === "reset_boundary" ? i : latest), -1);
|
|
292
|
+
|
|
287
293
|
// Build messages and collect corresponding entries
|
|
288
294
|
// When there's a compaction, we need to:
|
|
289
295
|
// 1. Emit summary first (entry = compaction)
|
|
@@ -379,6 +385,26 @@ export function buildSessionContext(
|
|
|
379
385
|
appendMessage(entry);
|
|
380
386
|
}
|
|
381
387
|
}
|
|
388
|
+
} else if (
|
|
389
|
+
resetBoundaryIdx >= 0 &&
|
|
390
|
+
resetBoundaryIdx > (compaction ? path.findIndex(e => e.type === "compaction" && e.id === compaction.id) : -1)
|
|
391
|
+
) {
|
|
392
|
+
// A `/reset` boundary durably starts emission after it — for BOTH the
|
|
393
|
+
// collapsed live transcript AND the model context (non-transcript) rebuild
|
|
394
|
+
// that feeds agent.replaceMessages (resume, /shake, reload, image drop).
|
|
395
|
+
// Without honoring it here, those model-context rebuilds walk the full
|
|
396
|
+
// persisted branch and put the pre-reset turns back into the LLM context
|
|
397
|
+
// even though `/reset` reported it empty. The full-history export path
|
|
398
|
+
// (`transcript && !collapseCompactedHistory`) is handled by the first
|
|
399
|
+
// branch above and left untouched, so on-disk history stays recoverable.
|
|
400
|
+
// When a compaction and a reset boundary interact, the later one on the
|
|
401
|
+
// path wins: a boundary after the latest compaction elides that compaction
|
|
402
|
+
// (and its kept tail) too, so only genuinely post-reset entries emit; a
|
|
403
|
+
// boundary before the latest compaction is superseded by it (the
|
|
404
|
+
// `else if (compaction)` branch below handles that case via this guard).
|
|
405
|
+
for (let i = resetBoundaryIdx + 1; i < path.length; i++) {
|
|
406
|
+
appendMessage(path[i]);
|
|
407
|
+
}
|
|
382
408
|
} else if (compaction) {
|
|
383
409
|
const providerPayload = getOpenAiRemoteCompactionPayload(compaction);
|
|
384
410
|
const remoteReplacementHistory = providerPayload?.items;
|
|
@@ -39,6 +39,8 @@ export interface SessionHeader {
|
|
|
39
39
|
*/
|
|
40
40
|
additionalDirectories?: string[];
|
|
41
41
|
parentSession?: string;
|
|
42
|
+
/** Prior absolute JSONL locations recorded by successful session moves. */
|
|
43
|
+
previousSessionFiles?: string[];
|
|
42
44
|
/** Provider prompt-cache identity inherited by exact-route full forks. */
|
|
43
45
|
providerPromptCacheKey?: string;
|
|
44
46
|
}
|
|
@@ -119,6 +121,18 @@ export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
|
|
|
119
121
|
fromExtension?: boolean;
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Pure marker entry recorded by `/reset` (resetSessionContext). It carries no
|
|
126
|
+
* payload — its presence on the branch is a durable boundary the collapsed
|
|
127
|
+
* live transcript and the model-context rebuild start emission after, so a
|
|
128
|
+
* rebuild (theme change, focus attach, /shake, resume) does not resurrect the
|
|
129
|
+
* pre-reset conversation. The on-disk record and the plain `transcript:true`
|
|
130
|
+
* export path keep the full pre-reset history.
|
|
131
|
+
*/
|
|
132
|
+
export interface ResetBoundaryEntry extends SessionEntryBase {
|
|
133
|
+
type: "reset_boundary";
|
|
134
|
+
}
|
|
135
|
+
|
|
122
136
|
/**
|
|
123
137
|
* Custom entry for extensions to store extension-specific data in the session.
|
|
124
138
|
* Use customType to identify your extension's entries.
|
|
@@ -155,6 +169,7 @@ declare module "@oh-my-pi/pi-agent-core/compaction/entries" {
|
|
|
155
169
|
interface CustomCompactionSessionEntries {
|
|
156
170
|
titleChange: TitleChangeEntry;
|
|
157
171
|
credentialPin: CredentialPinEntry;
|
|
172
|
+
resetBoundary: ResetBoundaryEntry;
|
|
158
173
|
}
|
|
159
174
|
}
|
|
160
175
|
|
|
@@ -250,7 +265,8 @@ export type SessionEntry =
|
|
|
250
265
|
| TtsrInjectionEntry
|
|
251
266
|
| SessionInitEntry
|
|
252
267
|
| ModeChangeEntry
|
|
253
|
-
| CredentialPinEntry
|
|
268
|
+
| CredentialPinEntry
|
|
269
|
+
| ResetBoundaryEntry;
|
|
254
270
|
|
|
255
271
|
/** Raw logical file entry after loaders strip any fixed-width title slot. */
|
|
256
272
|
export type FileEntry = SessionHeader | SessionEntry;
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
type ModeChangeEntry,
|
|
45
45
|
type ModelChangeEntry,
|
|
46
46
|
type NewSessionOptions,
|
|
47
|
+
type ResetBoundaryEntry,
|
|
47
48
|
type ServiceTierChangeEntry,
|
|
48
49
|
type SessionEntry,
|
|
49
50
|
type SessionHeader,
|
|
@@ -1465,6 +1466,12 @@ export class SessionManager {
|
|
|
1465
1466
|
throw err;
|
|
1466
1467
|
}
|
|
1467
1468
|
|
|
1469
|
+
if (sessionFileExisted && sessionPathChanged) {
|
|
1470
|
+
this.#header.previousSessionFiles = [
|
|
1471
|
+
...new Set([...(this.#header.previousSessionFiles ?? []), path.resolve(oldSessionFile)]),
|
|
1472
|
+
];
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1468
1475
|
this.#sessionFile = newSessionFile;
|
|
1469
1476
|
this.#artifactManager = null;
|
|
1470
1477
|
this.#artifactManagerSessionFile = null;
|
|
@@ -2086,6 +2093,18 @@ export class SessionManager {
|
|
|
2086
2093
|
return entry.id;
|
|
2087
2094
|
}
|
|
2088
2095
|
|
|
2096
|
+
/**
|
|
2097
|
+
* Append the durable conversation boundary recorded by `/reset`. The
|
|
2098
|
+
* collapsed live transcript and the model-context rebuild start after the
|
|
2099
|
+
* latest one, while the full history stays on disk (the plain
|
|
2100
|
+
* `transcript:true` export walks it unchanged).
|
|
2101
|
+
*/
|
|
2102
|
+
appendResetBoundary(): string {
|
|
2103
|
+
const entry: ResetBoundaryEntry = { type: "reset_boundary", ...this.#freshEntryFields() };
|
|
2104
|
+
this.#recordEntry(entry);
|
|
2105
|
+
return entry.id;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2089
2108
|
appendCustomEntry(customType: string, data?: unknown): string {
|
|
2090
2109
|
const entry: CustomEntry = { type: "custom", customType, data, ...this.#freshEntryFields() };
|
|
2091
2110
|
this.#recordEntry(entry);
|
|
@@ -2340,6 +2359,8 @@ export class SessionManager {
|
|
|
2340
2359
|
id: newSessionId,
|
|
2341
2360
|
timestamp,
|
|
2342
2361
|
cwd: this.#cwd,
|
|
2362
|
+
title: this.#sessionName,
|
|
2363
|
+
titleSource: this.#titleSource,
|
|
2343
2364
|
parentSession: this.#persist ? sourceSessionFile : undefined,
|
|
2344
2365
|
additionalDirectories: this.#additionalDirectories.length > 0 ? [...this.#additionalDirectories] : undefined,
|
|
2345
2366
|
};
|