@oh-my-pi/pi-coding-agent 13.18.0 → 13.19.0
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 +50 -0
- package/package.json +7 -11
- package/src/autoresearch/git.ts +25 -30
- package/src/autoresearch/tools/log-experiment.ts +61 -74
- package/src/commit/agentic/agent.ts +0 -3
- package/src/commit/agentic/index.ts +19 -22
- package/src/commit/agentic/tools/git-file-diff.ts +3 -6
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +6 -9
- package/src/commit/agentic/tools/index.ts +6 -8
- package/src/commit/agentic/tools/propose-commit.ts +4 -7
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/split-commit.ts +4 -4
- package/src/commit/changelog/index.ts +5 -9
- package/src/commit/pipeline.ts +10 -12
- package/src/config/keybindings.ts +7 -6
- package/src/config/settings-schema.ts +44 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +4 -16
- package/src/extensibility/custom-commands/bundled/review/index.ts +43 -41
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
- package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
- package/src/index.ts +1 -0
- package/src/main.ts +24 -2
- package/src/modes/components/footer.ts +9 -29
- package/src/modes/components/hook-editor.ts +3 -3
- package/src/modes/components/hook-selector.ts +6 -1
- package/src/modes/components/session-observer-overlay.ts +472 -0
- package/src/modes/components/settings-defs.ts +19 -0
- package/src/modes/components/status-line.ts +15 -61
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +59 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +26 -0
- package/src/modes/interactive-mode.ts +195 -43
- package/src/modes/session-observer-registry.ts +146 -0
- package/src/modes/shared.ts +0 -42
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/keybinding-matchers.ts +9 -0
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/sdk.ts +28 -13
- package/src/secrets/index.ts +1 -1
- package/src/secrets/obfuscator.ts +24 -16
- package/src/session/agent-session.ts +75 -30
- package/src/session/session-manager.ts +15 -5
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +28 -0
- package/src/task/index.ts +88 -78
- package/src/task/types.ts +25 -0
- package/src/task/worktree.ts +127 -145
- package/src/tools/exit-plan-mode.ts +1 -0
- package/src/tools/gh.ts +120 -297
- package/src/tools/read.ts +13 -79
- package/src/utils/external-editor.ts +11 -5
- package/src/utils/git.ts +1400 -0
- package/src/web/search/render.ts +6 -4
- package/src/commit/git/errors.ts +0 -9
- package/src/commit/git/index.ts +0 -210
- package/src/commit/git/operations.ts +0 -54
- package/src/tools/gh-cli.ts +0 -125
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
extractExplicitThinkingSelector,
|
|
60
60
|
formatModelString,
|
|
61
61
|
parseModelString,
|
|
62
|
+
type ResolvedModelRoleValue,
|
|
62
63
|
resolveModelRoleValue,
|
|
63
64
|
} from "../config/model-resolver";
|
|
64
65
|
import { expandPromptTemplate, type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
|
|
@@ -114,7 +115,7 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
|
|
114
115
|
type: "text",
|
|
115
116
|
};
|
|
116
117
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
117
|
-
import type
|
|
118
|
+
import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
|
|
118
119
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
119
120
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
120
121
|
import { outputMeta } from "../tools/output-meta";
|
|
@@ -162,7 +163,7 @@ import { getLatestCompactionEntry } from "./session-manager";
|
|
|
162
163
|
/** Session-specific events that extend the core AgentEvent */
|
|
163
164
|
export type AgentSessionEvent =
|
|
164
165
|
| AgentEvent
|
|
165
|
-
| { type: "auto_compaction_start"; reason: "threshold" | "overflow"; action: "context-full" | "handoff" }
|
|
166
|
+
| { type: "auto_compaction_start"; reason: "threshold" | "overflow" | "idle"; action: "context-full" | "handoff" }
|
|
166
167
|
| {
|
|
167
168
|
type: "auto_compaction_end";
|
|
168
169
|
action: "context-full" | "handoff";
|
|
@@ -539,7 +540,7 @@ export class AgentSession {
|
|
|
539
540
|
this.#defaultSelectedMCPServerNames = new Set(config.defaultSelectedMCPServerNames ?? []);
|
|
540
541
|
this.#defaultSelectedMCPToolNames = new Set(config.defaultSelectedMCPToolNames ?? []);
|
|
541
542
|
this.#pruneSelectedMCPToolNames();
|
|
542
|
-
const persistedSelectedMCPToolNames = this.
|
|
543
|
+
const persistedSelectedMCPToolNames = this.buildDisplaySessionContext().selectedMCPToolNames;
|
|
543
544
|
const currentSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
544
545
|
const persistInitialMCPToolSelection =
|
|
545
546
|
config.persistInitialMCPToolSelection ?? this.sessionManager.getBranch().length === 0;
|
|
@@ -668,7 +669,22 @@ export class AgentSession {
|
|
|
668
669
|
}
|
|
669
670
|
}
|
|
670
671
|
|
|
671
|
-
|
|
672
|
+
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
673
|
+
// obfuscated placeholders, but listeners (TUI, extensions, exporters) must see real
|
|
674
|
+
// values. The original event.message stays obfuscated so the persistence path below
|
|
675
|
+
// writes `#HASH#` tokens to the session file; convertToLlm re-obfuscates outbound
|
|
676
|
+
// traffic on the next turn. Walks text, thinking, and toolCall arguments/intent.
|
|
677
|
+
let displayEvent: AgentEvent = event;
|
|
678
|
+
const obfuscator = this.#obfuscator;
|
|
679
|
+
if (obfuscator && event.type === "message_end" && event.message.role === "assistant") {
|
|
680
|
+
const message = event.message;
|
|
681
|
+
const deobfuscatedContent = obfuscator.deobfuscateObject(message.content);
|
|
682
|
+
if (deobfuscatedContent !== message.content) {
|
|
683
|
+
displayEvent = { ...event, message: { ...message, content: deobfuscatedContent } };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
await this.#emitSessionEvent(displayEvent);
|
|
672
688
|
|
|
673
689
|
if (event.type === "turn_start") {
|
|
674
690
|
this.#resetStreamingEditState();
|
|
@@ -1968,7 +1984,7 @@ export class AgentSession {
|
|
|
1968
1984
|
|
|
1969
1985
|
this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
|
|
1970
1986
|
this.#pruneSelectedMCPToolNames();
|
|
1971
|
-
if (!this.
|
|
1987
|
+
if (!this.buildDisplaySessionContext().hasPersistedMCPToolSelection) {
|
|
1972
1988
|
this.#selectedMCPToolNames = new Set([
|
|
1973
1989
|
...this.#selectedMCPToolNames,
|
|
1974
1990
|
...this.#getConfiguredDefaultSelectedMCPToolNames(),
|
|
@@ -1993,6 +2009,10 @@ export class AgentSession {
|
|
|
1993
2009
|
return this.agent.state.messages;
|
|
1994
2010
|
}
|
|
1995
2011
|
|
|
2012
|
+
buildDisplaySessionContext(): SessionContext {
|
|
2013
|
+
return deobfuscateSessionContext(this.sessionManager.buildSessionContext(), this.#obfuscator);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1996
2016
|
/** Convert session messages using the same pre-LLM pipeline as the active session. */
|
|
1997
2017
|
async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
|
|
1998
2018
|
const transformedMessages = await this.#transformContext(messages, signal);
|
|
@@ -2103,7 +2123,16 @@ export class AgentSession {
|
|
|
2103
2123
|
}
|
|
2104
2124
|
|
|
2105
2125
|
resolveRoleModel(role: string): Model | undefined {
|
|
2106
|
-
return this.#
|
|
2126
|
+
return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model).model;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/**
|
|
2130
|
+
* Resolve a role to its model AND thinking level.
|
|
2131
|
+
* Unlike resolveRoleModel(), this preserves the thinking level suffix
|
|
2132
|
+
* from role configuration (e.g., "anthropic/claude-sonnet-4-5:xhigh").
|
|
2133
|
+
*/
|
|
2134
|
+
resolveRoleModelWithThinking(role: string): ResolvedModelRoleValue {
|
|
2135
|
+
return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model);
|
|
2107
2136
|
}
|
|
2108
2137
|
|
|
2109
2138
|
get promptTemplates(): ReadonlyArray<PromptTemplate> {
|
|
@@ -3182,7 +3211,7 @@ export class AgentSession {
|
|
|
3182
3211
|
* Validates API key, saves to session log but NOT to settings.
|
|
3183
3212
|
* @throws Error if no API key available for the model
|
|
3184
3213
|
*/
|
|
3185
|
-
async setModelTemporary(model: Model): Promise<void> {
|
|
3214
|
+
async setModelTemporary(model: Model, thinkingLevel?: ThinkingLevel): Promise<void> {
|
|
3186
3215
|
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
3187
3216
|
if (!apiKey) {
|
|
3188
3217
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
@@ -3193,8 +3222,8 @@ export class AgentSession {
|
|
|
3193
3222
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
|
|
3194
3223
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
3195
3224
|
|
|
3196
|
-
//
|
|
3197
|
-
this.setThinkingLevel(this.thinkingLevel);
|
|
3225
|
+
// Apply explicit thinking level, or re-clamp current level to new model's capabilities
|
|
3226
|
+
this.setThinkingLevel(thinkingLevel ?? this.thinkingLevel);
|
|
3198
3227
|
}
|
|
3199
3228
|
|
|
3200
3229
|
/**
|
|
@@ -3267,13 +3296,12 @@ export class AgentSession {
|
|
|
3267
3296
|
const next = roleModels[nextIndex];
|
|
3268
3297
|
|
|
3269
3298
|
if (options?.temporary) {
|
|
3270
|
-
await this.setModelTemporary(next.model);
|
|
3299
|
+
await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined);
|
|
3271
3300
|
} else {
|
|
3272
3301
|
await this.setModel(next.model, next.role);
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
this.setThinkingLevel(next.thinkingLevel);
|
|
3302
|
+
if (next.explicitThinkingLevel && next.thinkingLevel !== undefined) {
|
|
3303
|
+
this.setThinkingLevel(next.thinkingLevel);
|
|
3304
|
+
}
|
|
3277
3305
|
}
|
|
3278
3306
|
|
|
3279
3307
|
return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
|
|
@@ -3473,7 +3501,7 @@ export class AgentSession {
|
|
|
3473
3501
|
}
|
|
3474
3502
|
|
|
3475
3503
|
await this.sessionManager.rewriteEntries();
|
|
3476
|
-
const sessionContext = this.
|
|
3504
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
3477
3505
|
this.agent.replaceMessages(sessionContext.messages);
|
|
3478
3506
|
this.#syncTodoPhasesFromBranch();
|
|
3479
3507
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
@@ -3599,7 +3627,7 @@ export class AgentSession {
|
|
|
3599
3627
|
preserveData,
|
|
3600
3628
|
);
|
|
3601
3629
|
const newEntries = this.sessionManager.getEntries();
|
|
3602
|
-
const sessionContext = this.
|
|
3630
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
3603
3631
|
this.agent.replaceMessages(sessionContext.messages);
|
|
3604
3632
|
this.#syncTodoPhasesFromBranch();
|
|
3605
3633
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
@@ -3646,6 +3674,12 @@ export class AgentSession {
|
|
|
3646
3674
|
this.#handoffAbortController?.abort();
|
|
3647
3675
|
}
|
|
3648
3676
|
|
|
3677
|
+
/** Trigger idle compaction through the auto-compaction flow (with UI events). */
|
|
3678
|
+
async runIdleCompaction(): Promise<void> {
|
|
3679
|
+
if (this.isStreaming || this.isCompacting) return;
|
|
3680
|
+
await this.#runAutoCompaction("idle", false, true);
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3649
3683
|
/**
|
|
3650
3684
|
* Cancel in-progress branch summarization.
|
|
3651
3685
|
*/
|
|
@@ -3814,7 +3848,7 @@ export class AgentSession {
|
|
|
3814
3848
|
}
|
|
3815
3849
|
|
|
3816
3850
|
// Rebuild agent messages from session
|
|
3817
|
-
const sessionContext = this.
|
|
3851
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
3818
3852
|
this.agent.replaceMessages(sessionContext.messages);
|
|
3819
3853
|
this.#syncTodoPhasesFromBranch();
|
|
3820
3854
|
|
|
@@ -4364,19 +4398,25 @@ export class AgentSession {
|
|
|
4364
4398
|
return availableModels.find(m => m.provider === currentModel.provider && m.id === configuredTarget);
|
|
4365
4399
|
}
|
|
4366
4400
|
|
|
4367
|
-
#
|
|
4401
|
+
#resolveRoleModelFull(
|
|
4402
|
+
role: string,
|
|
4403
|
+
availableModels: Model[],
|
|
4404
|
+
currentModel: Model | undefined,
|
|
4405
|
+
): ResolvedModelRoleValue {
|
|
4368
4406
|
const roleModelStr =
|
|
4369
4407
|
role === "default"
|
|
4370
4408
|
? (this.settings.getModelRole("default") ??
|
|
4371
4409
|
(currentModel ? `${currentModel.provider}/${currentModel.id}` : undefined))
|
|
4372
4410
|
: this.settings.getModelRole(role);
|
|
4373
4411
|
|
|
4374
|
-
if (!roleModelStr)
|
|
4412
|
+
if (!roleModelStr) {
|
|
4413
|
+
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
|
|
4414
|
+
}
|
|
4375
4415
|
|
|
4376
4416
|
return resolveModelRoleValue(roleModelStr, availableModels, {
|
|
4377
4417
|
settings: this.settings,
|
|
4378
4418
|
matchPreferences: { usageOrder: this.settings.getStorage()?.getModelUsageOrder() },
|
|
4379
|
-
})
|
|
4419
|
+
});
|
|
4380
4420
|
}
|
|
4381
4421
|
|
|
4382
4422
|
#getCompactionModelCandidates(availableModels: Model[]): Model[] {
|
|
@@ -4393,7 +4433,7 @@ export class AgentSession {
|
|
|
4393
4433
|
|
|
4394
4434
|
const currentModel = this.model;
|
|
4395
4435
|
for (const role of MODEL_ROLE_IDS) {
|
|
4396
|
-
addCandidate(this.#
|
|
4436
|
+
addCandidate(this.#resolveRoleModelFull(role, availableModels, currentModel).model);
|
|
4397
4437
|
}
|
|
4398
4438
|
|
|
4399
4439
|
const sortedByContext = [...availableModels].sort((a, b) => b.contextWindow - a.contextWindow);
|
|
@@ -4410,11 +4450,16 @@ export class AgentSession {
|
|
|
4410
4450
|
/**
|
|
4411
4451
|
* Internal: Run auto-compaction with events.
|
|
4412
4452
|
*/
|
|
4413
|
-
async #runAutoCompaction(
|
|
4453
|
+
async #runAutoCompaction(
|
|
4454
|
+
reason: "overflow" | "threshold" | "idle",
|
|
4455
|
+
willRetry: boolean,
|
|
4456
|
+
deferred = false,
|
|
4457
|
+
): Promise<void> {
|
|
4414
4458
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
4415
|
-
if (
|
|
4459
|
+
if (compactionSettings.strategy === "off") return;
|
|
4460
|
+
if (reason !== "idle" && !compactionSettings.enabled) return;
|
|
4416
4461
|
const generation = this.#promptGeneration;
|
|
4417
|
-
if (!deferred && reason !== "overflow" && compactionSettings.strategy === "handoff") {
|
|
4462
|
+
if (!deferred && reason !== "overflow" && reason !== "idle" && compactionSettings.strategy === "handoff") {
|
|
4418
4463
|
this.#schedulePostPromptTask(
|
|
4419
4464
|
async signal => {
|
|
4420
4465
|
await Promise.resolve();
|
|
@@ -4689,7 +4734,7 @@ export class AgentSession {
|
|
|
4689
4734
|
preserveData,
|
|
4690
4735
|
);
|
|
4691
4736
|
const newEntries = this.sessionManager.getEntries();
|
|
4692
|
-
const sessionContext = this.
|
|
4737
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
4693
4738
|
this.agent.replaceMessages(sessionContext.messages);
|
|
4694
4739
|
this.#syncTodoPhasesFromBranch();
|
|
4695
4740
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
@@ -4717,7 +4762,7 @@ export class AgentSession {
|
|
|
4717
4762
|
};
|
|
4718
4763
|
await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
|
|
4719
4764
|
|
|
4720
|
-
if (!willRetry && compactionSettings.autoContinue !== false) {
|
|
4765
|
+
if (!willRetry && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
4721
4766
|
const continuePrompt = async () => {
|
|
4722
4767
|
await this.#promptWithMessage(
|
|
4723
4768
|
{
|
|
@@ -5516,7 +5561,7 @@ export class AgentSession {
|
|
|
5516
5561
|
// Flush pending writes before switching so restore snapshots reflect committed state.
|
|
5517
5562
|
await this.sessionManager.flush();
|
|
5518
5563
|
const previousSessionState = this.sessionManager.captureState();
|
|
5519
|
-
const previousSessionContext = this.
|
|
5564
|
+
const previousSessionContext = this.buildDisplaySessionContext();
|
|
5520
5565
|
// switchSession replaces these arrays wholesale during load/rollback, so retaining
|
|
5521
5566
|
// the existing message objects is sufficient and avoids structured-clone failures for
|
|
5522
5567
|
// extension/custom metadata that is valid to persist but not cloneable.
|
|
@@ -5545,7 +5590,7 @@ export class AgentSession {
|
|
|
5545
5590
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
5546
5591
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
5547
5592
|
|
|
5548
|
-
const sessionContext = this.
|
|
5593
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
5549
5594
|
const didReloadConversationChange =
|
|
5550
5595
|
!switchingToDifferentSession &&
|
|
5551
5596
|
this.#didSessionMessagesChange(previousSessionContext.messages, sessionContext.messages);
|
|
@@ -5711,7 +5756,7 @@ export class AgentSession {
|
|
|
5711
5756
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
5712
5757
|
|
|
5713
5758
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
5714
|
-
const sessionContext = this.
|
|
5759
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
5715
5760
|
|
|
5716
5761
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
5717
5762
|
|
|
@@ -5882,7 +5927,7 @@ export class AgentSession {
|
|
|
5882
5927
|
}
|
|
5883
5928
|
|
|
5884
5929
|
// Update agent state
|
|
5885
|
-
const sessionContext = this.
|
|
5930
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
5886
5931
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
5887
5932
|
this.agent.replaceMessages(sessionContext.messages);
|
|
5888
5933
|
this.#syncTodoPhasesFromBranch();
|
|
@@ -1390,6 +1390,7 @@ export class SessionManager {
|
|
|
1390
1390
|
#sessionFile: string | undefined;
|
|
1391
1391
|
#flushed: boolean = false;
|
|
1392
1392
|
#needsFullRewriteOnNextPersist: boolean = false;
|
|
1393
|
+
#ensuredOnDisk: boolean = false;
|
|
1393
1394
|
#fileEntries: FileEntry[] = [];
|
|
1394
1395
|
#byId: Map<string, SessionEntry> = new Map();
|
|
1395
1396
|
#labelsById: Map<string, string> = new Map();
|
|
@@ -1496,12 +1497,14 @@ export class SessionManager {
|
|
|
1496
1497
|
|
|
1497
1498
|
this.#buildIndex();
|
|
1498
1499
|
this.#flushed = true;
|
|
1500
|
+
this.#ensuredOnDisk = true;
|
|
1499
1501
|
} else {
|
|
1500
1502
|
const explicitPath = this.#sessionFile;
|
|
1501
1503
|
this.#newSessionSync();
|
|
1502
1504
|
this.#sessionFile = explicitPath; // preserve explicit path from --session flag
|
|
1503
1505
|
await this.#rewriteFile();
|
|
1504
1506
|
this.#flushed = true;
|
|
1507
|
+
this.#ensuredOnDisk = true;
|
|
1505
1508
|
return;
|
|
1506
1509
|
}
|
|
1507
1510
|
}
|
|
@@ -1678,6 +1681,7 @@ export class SessionManager {
|
|
|
1678
1681
|
this.#leafId = null;
|
|
1679
1682
|
this.#flushed = false;
|
|
1680
1683
|
this.#needsFullRewriteOnNextPersist = false;
|
|
1684
|
+
this.#ensuredOnDisk = false;
|
|
1681
1685
|
this.#usageStatistics = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, premiumRequests: 0, cost: 0 };
|
|
1682
1686
|
this.#inMemoryArtifacts = null;
|
|
1683
1687
|
this.#inMemoryArtifactCounter = 0;
|
|
@@ -1839,6 +1843,7 @@ export class SessionManager {
|
|
|
1839
1843
|
if (!this.persist || !this.#sessionFile) return;
|
|
1840
1844
|
if (this.#flushed && !this.#needsFullRewriteOnNextPersist) return;
|
|
1841
1845
|
await this.#rewriteFile();
|
|
1846
|
+
this.#ensuredOnDisk = true;
|
|
1842
1847
|
}
|
|
1843
1848
|
|
|
1844
1849
|
/** Flush pending writes to disk. Call before switching sessions or on shutdown. */
|
|
@@ -1972,11 +1977,16 @@ export class SessionManager {
|
|
|
1972
1977
|
if (!this.persist || !this.#sessionFile) return;
|
|
1973
1978
|
if (this.#persistError) throw this.#persistError;
|
|
1974
1979
|
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
+
// Normally we wait for the first assistant message before persisting to avoid
|
|
1981
|
+
// creating files for sessions that never produce output. Once ensureOnDisk() has
|
|
1982
|
+
// been called, the session is already on disk and every entry must be flushed.
|
|
1983
|
+
if (!this.#ensuredOnDisk) {
|
|
1984
|
+
const hasAssistant = this.#fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
|
|
1985
|
+
if (!hasAssistant) {
|
|
1986
|
+
// Mark as not flushed so when assistant arrives, all entries get written.
|
|
1987
|
+
this.#flushed = false;
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1980
1990
|
}
|
|
1981
1991
|
|
|
1982
1992
|
if (this.#needsFullRewriteOnNextPersist || !this.#flushed) {
|
package/src/system-prompt.ts
CHANGED
|
@@ -432,6 +432,8 @@ export interface BuildSystemPromptOptions {
|
|
|
432
432
|
eagerTasks?: boolean;
|
|
433
433
|
/** Rules with alwaysApply=true — their full content is injected into the prompt. */
|
|
434
434
|
alwaysApplyRules?: AlwaysApplyRule[];
|
|
435
|
+
/** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
|
|
436
|
+
secretsEnabled?: boolean;
|
|
435
437
|
}
|
|
436
438
|
|
|
437
439
|
/** Build the system prompt with tools, guidelines, and context */
|
|
@@ -456,6 +458,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
456
458
|
mcpDiscoveryMode = false,
|
|
457
459
|
mcpDiscoveryServerSummaries = [],
|
|
458
460
|
eagerTasks = false,
|
|
461
|
+
secretsEnabled = false,
|
|
459
462
|
} = options;
|
|
460
463
|
const resolvedCwd = cwd ?? getProjectDir();
|
|
461
464
|
|
|
@@ -603,6 +606,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
603
606
|
hasMCPDiscoveryServers: mcpDiscoveryServerSummaries.length > 0,
|
|
604
607
|
mcpDiscoveryServerSummaries,
|
|
605
608
|
eagerTasks,
|
|
609
|
+
secretsEnabled,
|
|
606
610
|
};
|
|
607
611
|
return renderPromptTemplate(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
|
|
608
612
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type ReviewFinding,
|
|
39
39
|
type SingleResult,
|
|
40
40
|
TASK_SUBAGENT_EVENT_CHANNEL,
|
|
41
|
+
TASK_SUBAGENT_LIFECYCLE_CHANNEL,
|
|
41
42
|
TASK_SUBAGENT_PROGRESS_CHANNEL,
|
|
42
43
|
} from "./types";
|
|
43
44
|
|
|
@@ -630,6 +631,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
630
631
|
task,
|
|
631
632
|
assignment,
|
|
632
633
|
progress: { ...progress },
|
|
634
|
+
sessionFile: subtaskSessionFile,
|
|
633
635
|
});
|
|
634
636
|
}
|
|
635
637
|
lastProgressEmitMs = Date.now();
|
|
@@ -983,6 +985,19 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
983
985
|
|
|
984
986
|
activeSession = session;
|
|
985
987
|
|
|
988
|
+
// Emit lifecycle start event
|
|
989
|
+
if (options.eventBus) {
|
|
990
|
+
options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
|
|
991
|
+
id,
|
|
992
|
+
agent: agent.name,
|
|
993
|
+
agentSource: agent.source,
|
|
994
|
+
description: options.description,
|
|
995
|
+
status: "started",
|
|
996
|
+
sessionFile: subtaskSessionFile,
|
|
997
|
+
index,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
986
1001
|
const subagentToolNames = session.getActiveToolNames();
|
|
987
1002
|
const parentOwnedToolNames = new Set(["todo_write"]);
|
|
988
1003
|
const filteredSubagentTools = subagentToolNames.filter(name => !parentOwnedToolNames.has(name));
|
|
@@ -1238,6 +1253,19 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1238
1253
|
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
|
1239
1254
|
scheduleProgress(true);
|
|
1240
1255
|
|
|
1256
|
+
// Emit lifecycle end event after finalization so submit_result status is reflected
|
|
1257
|
+
if (options.eventBus) {
|
|
1258
|
+
options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
|
|
1259
|
+
id,
|
|
1260
|
+
agent: agent.name,
|
|
1261
|
+
agentSource: agent.source,
|
|
1262
|
+
description: options.description,
|
|
1263
|
+
status: progress.status as "completed" | "failed" | "aborted",
|
|
1264
|
+
sessionFile: subtaskSessionFile,
|
|
1265
|
+
index,
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1241
1269
|
return {
|
|
1242
1270
|
index,
|
|
1243
1271
|
id,
|
package/src/task/index.ts
CHANGED
|
@@ -18,7 +18,6 @@ import path from "node:path";
|
|
|
18
18
|
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
19
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
20
20
|
import { $env, Snowflake } from "@oh-my-pi/pi-utils";
|
|
21
|
-
import { $ } from "bun";
|
|
22
21
|
import type { ToolSession } from "..";
|
|
23
22
|
import { resolveAgentModelPatterns } from "../config/model-resolver";
|
|
24
23
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
@@ -30,6 +29,7 @@ import { formatBytes, formatDuration } from "../tools/render-utils";
|
|
|
30
29
|
// Import review tools for side effects (registers subagent tool handlers)
|
|
31
30
|
import "../tools/review";
|
|
32
31
|
import { generateCommitMessage } from "../utils/commit-message-generator";
|
|
32
|
+
import * as git from "../utils/git";
|
|
33
33
|
import { discoverAgents, getAgent } from "./discovery";
|
|
34
34
|
import { runSubprocess } from "./executor";
|
|
35
35
|
import { resolveIsolationBackendForTaskExecution } from "./isolation-backend";
|
|
@@ -109,8 +109,21 @@ export { loadBundledAgents as BUNDLED_AGENTS } from "./agents";
|
|
|
109
109
|
export { discoverCommands, expandCommand, getCommand } from "./commands";
|
|
110
110
|
export { discoverAgents, getAgent } from "./discovery";
|
|
111
111
|
export { AgentOutputManager } from "./output-manager";
|
|
112
|
-
export type {
|
|
113
|
-
|
|
112
|
+
export type {
|
|
113
|
+
AgentDefinition,
|
|
114
|
+
AgentProgress,
|
|
115
|
+
SingleResult,
|
|
116
|
+
SubagentLifecyclePayload,
|
|
117
|
+
SubagentProgressPayload,
|
|
118
|
+
TaskParams,
|
|
119
|
+
TaskToolDetails,
|
|
120
|
+
} from "./types";
|
|
121
|
+
export {
|
|
122
|
+
TASK_SUBAGENT_EVENT_CHANNEL,
|
|
123
|
+
TASK_SUBAGENT_LIFECYCLE_CHANNEL,
|
|
124
|
+
TASK_SUBAGENT_PROGRESS_CHANNEL,
|
|
125
|
+
taskSchema,
|
|
126
|
+
} from "./types";
|
|
114
127
|
|
|
115
128
|
/**
|
|
116
129
|
* Render the tool description from a cached agent list and current settings.
|
|
@@ -766,7 +779,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
766
779
|
contextFile: contextFilePath,
|
|
767
780
|
enableLsp: false,
|
|
768
781
|
signal,
|
|
769
|
-
eventBus:
|
|
782
|
+
eventBus: this.session.eventBus,
|
|
770
783
|
onProgress: progress => {
|
|
771
784
|
progressMap.set(index, {
|
|
772
785
|
...structuredClone(progress),
|
|
@@ -820,7 +833,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
820
833
|
contextFile: contextFilePath,
|
|
821
834
|
enableLsp: false,
|
|
822
835
|
signal,
|
|
823
|
-
eventBus:
|
|
836
|
+
eventBus: this.session.eventBus,
|
|
824
837
|
onProgress: progress => {
|
|
825
838
|
progressMap.set(index, {
|
|
826
839
|
...structuredClone(progress),
|
|
@@ -864,7 +877,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
864
877
|
} catch (mergeErr) {
|
|
865
878
|
// Agent succeeded but branch commit failed — clean up stale branch
|
|
866
879
|
const branchName = `omp/task/${task.id}`;
|
|
867
|
-
await
|
|
880
|
+
await git.branch.tryDelete(repoRoot, branchName);
|
|
868
881
|
const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
869
882
|
return { ...result, error: `Merge failed: ${msg}` };
|
|
870
883
|
}
|
|
@@ -978,93 +991,90 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
978
991
|
let changesApplied: boolean | null = null;
|
|
979
992
|
let mergedBranchesForNestedPatches: Set<string> | null = null;
|
|
980
993
|
if (isIsolated && repoRoot) {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
if (branchEntries.length === 0) {
|
|
988
|
-
changesApplied = true;
|
|
989
|
-
} else {
|
|
990
|
-
const mergeResult = await mergeTaskBranches(repoRoot, branchEntries);
|
|
991
|
-
mergedBranchesForNestedPatches = new Set(mergeResult.merged);
|
|
992
|
-
changesApplied = mergeResult.failed.length === 0;
|
|
994
|
+
try {
|
|
995
|
+
if (mergeMode === "branch") {
|
|
996
|
+
// Branch mode: merge task branches sequentially
|
|
997
|
+
const branchEntries = results
|
|
998
|
+
.filter(r => r.branchName && r.exitCode === 0 && !r.aborted)
|
|
999
|
+
.map(r => ({ branchName: r.branchName!, taskId: r.id, description: r.description }));
|
|
993
1000
|
|
|
994
|
-
if (
|
|
995
|
-
|
|
1001
|
+
if (branchEntries.length === 0) {
|
|
1002
|
+
changesApplied = true;
|
|
996
1003
|
} else {
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1004
|
+
const mergeResult = await mergeTaskBranches(repoRoot, branchEntries);
|
|
1005
|
+
mergedBranchesForNestedPatches = new Set(mergeResult.merged);
|
|
1006
|
+
changesApplied = mergeResult.failed.length === 0;
|
|
1007
|
+
|
|
1008
|
+
if (changesApplied) {
|
|
1009
|
+
mergeSummary = `\n\nMerged ${mergeResult.merged.length} branch${mergeResult.merged.length === 1 ? "" : "es"}: ${mergeResult.merged.join(", ")}`;
|
|
1010
|
+
} else {
|
|
1011
|
+
const mergedPart =
|
|
1012
|
+
mergeResult.merged.length > 0 ? `Merged: ${mergeResult.merged.join(", ")}.\n` : "";
|
|
1013
|
+
const failedPart = `Failed: ${mergeResult.failed.join(", ")}.`;
|
|
1014
|
+
const conflictPart = mergeResult.conflict ? `\nConflict: ${mergeResult.conflict}` : "";
|
|
1015
|
+
mergeSummary = `\n\n<system-notification>Branch merge failed. ${mergedPart}${failedPart}${conflictPart}\nUnmerged branches remain for manual resolution.</system-notification>`;
|
|
1016
|
+
}
|
|
1002
1017
|
}
|
|
1003
|
-
}
|
|
1004
1018
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
} else {
|
|
1011
|
-
// Patch mode: combine and apply patches
|
|
1012
|
-
const patchesInOrder = results.map(result => result.patchPath).filter(Boolean) as string[];
|
|
1013
|
-
const missingPatch = results.some(result => !result.patchPath);
|
|
1014
|
-
if (missingPatch) {
|
|
1015
|
-
changesApplied = false;
|
|
1019
|
+
// Clean up merged branches (keep failed ones for manual resolution)
|
|
1020
|
+
const allBranches = branchEntries.map(b => b.branchName);
|
|
1021
|
+
if (changesApplied) {
|
|
1022
|
+
await cleanupTaskBranches(repoRoot, allBranches);
|
|
1023
|
+
}
|
|
1016
1024
|
} else {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
);
|
|
1023
|
-
const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
|
|
1024
|
-
if (nonEmptyPatches.length === 0) {
|
|
1025
|
-
changesApplied = true;
|
|
1025
|
+
// Patch mode: combine and apply patches
|
|
1026
|
+
const patchesInOrder = results.map(result => result.patchPath).filter(Boolean) as string[];
|
|
1027
|
+
const missingPatch = results.some(result => !result.patchPath);
|
|
1028
|
+
if (missingPatch) {
|
|
1029
|
+
changesApplied = false;
|
|
1026
1030
|
} else {
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1031
|
+
const patchStats = await Promise.all(
|
|
1032
|
+
patchesInOrder.map(async patchPath => ({
|
|
1033
|
+
patchPath,
|
|
1034
|
+
size: (await fs.stat(patchPath)).size,
|
|
1035
|
+
})),
|
|
1029
1036
|
);
|
|
1030
|
-
const
|
|
1031
|
-
if (
|
|
1037
|
+
const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
|
|
1038
|
+
if (nonEmptyPatches.length === 0) {
|
|
1032
1039
|
changesApplied = true;
|
|
1033
1040
|
} else {
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
.
|
|
1047
|
-
|
|
1048
|
-
|
|
1041
|
+
const patchTexts = await Promise.all(
|
|
1042
|
+
nonEmptyPatches.map(async patchPath => Bun.file(patchPath).text()),
|
|
1043
|
+
);
|
|
1044
|
+
const combinedPatch = patchTexts
|
|
1045
|
+
.map(text => (text.endsWith("\n") ? text : `${text}\n`))
|
|
1046
|
+
.join("");
|
|
1047
|
+
if (!combinedPatch.trim()) {
|
|
1048
|
+
changesApplied = true;
|
|
1049
|
+
} else {
|
|
1050
|
+
changesApplied = await git.patch.canApplyText(repoRoot, combinedPatch);
|
|
1051
|
+
if (changesApplied) {
|
|
1052
|
+
try {
|
|
1053
|
+
await git.patch.applyText(repoRoot, combinedPatch);
|
|
1054
|
+
} catch {
|
|
1055
|
+
changesApplied = false;
|
|
1056
|
+
}
|
|
1049
1057
|
}
|
|
1050
|
-
} finally {
|
|
1051
|
-
await fs.rm(combinedPatchPath, { force: true });
|
|
1052
1058
|
}
|
|
1053
1059
|
}
|
|
1054
1060
|
}
|
|
1055
|
-
}
|
|
1056
1061
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1062
|
+
if (changesApplied) {
|
|
1063
|
+
mergeSummary = "\n\nApplied patches: yes";
|
|
1064
|
+
} else {
|
|
1065
|
+
const notification =
|
|
1066
|
+
"<system-notification>Patches were not applied and must be handled manually.</system-notification>";
|
|
1067
|
+
const patchList =
|
|
1068
|
+
patchPaths.length > 0
|
|
1069
|
+
? `\n\nPatch artifacts:\n${patchPaths.map(patch => `- ${patch}`).join("\n")}`
|
|
1070
|
+
: "";
|
|
1071
|
+
mergeSummary = `\n\n${notification}${patchList}`;
|
|
1072
|
+
}
|
|
1067
1073
|
}
|
|
1074
|
+
} catch (mergeErr) {
|
|
1075
|
+
const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
|
|
1076
|
+
changesApplied = false;
|
|
1077
|
+
mergeSummary = `\n\n<system-notification>Merge phase failed: ${msg}\nTask outputs are preserved but changes were not applied.</system-notification>`;
|
|
1068
1078
|
}
|
|
1069
1079
|
}
|
|
1070
1080
|
|