@oh-my-pi/pi-coding-agent 14.9.2 → 14.9.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 +89 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +3 -3
- package/src/async/job-manager.ts +66 -9
- package/src/capability/rule.ts +20 -0
- package/src/config/model-registry.ts +13 -0
- package/src/config/model-resolver.ts +8 -2
- package/src/config/prompt-templates.ts +0 -5
- package/src/config/settings-schema.ts +39 -1
- package/src/edit/index.ts +8 -0
- package/src/edit/renderer.ts +6 -1
- package/src/edit/streaming.ts +53 -2
- package/src/eval/eval.lark +10 -31
- package/src/eval/index.ts +1 -0
- package/src/eval/js/context-manager.ts +1 -38
- package/src/eval/js/prelude.txt +0 -2
- package/src/eval/parse.ts +156 -255
- package/src/eval/py/executor.ts +24 -8
- package/src/eval/py/index.ts +1 -0
- package/src/eval/py/prelude.py +11 -80
- package/src/eval/sniff.ts +28 -0
- package/src/export/html/template.css +50 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +229 -17
- package/src/extensibility/plugins/loader.ts +31 -6
- package/src/extensibility/skills.ts +20 -0
- package/src/hashline/constants.ts +20 -0
- package/src/hashline/grammar.lark +16 -23
- package/src/hashline/hash.ts +4 -34
- package/src/hashline/input.ts +16 -2
- package/src/hashline/parser.ts +12 -1
- package/src/internal-urls/agent-protocol.ts +64 -52
- package/src/internal-urls/artifact-protocol.ts +52 -51
- package/src/internal-urls/docs-index.generated.ts +34 -1
- package/src/internal-urls/index.ts +6 -19
- package/src/internal-urls/local-protocol.ts +50 -7
- package/src/internal-urls/mcp-protocol.ts +3 -8
- package/src/internal-urls/memory-protocol.ts +90 -59
- package/src/internal-urls/pi-protocol.ts +1 -0
- package/src/internal-urls/router.ts +40 -23
- package/src/internal-urls/rule-protocol.ts +3 -20
- package/src/internal-urls/skill-protocol.ts +5 -27
- package/src/internal-urls/types.ts +18 -2
- package/src/main.ts +1 -1
- package/src/mcp/manager.ts +17 -0
- package/src/modes/components/session-observer-overlay.ts +2 -2
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/components/tree-selector.ts +4 -0
- package/src/modes/controllers/event-controller.ts +23 -2
- package/src/modes/controllers/mcp-command-controller.ts +7 -10
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/theme/theme.ts +27 -27
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +14 -9
- package/src/prompts/commands/orchestrate.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +0 -2
- package/src/prompts/system/project-prompt.md +10 -0
- package/src/prompts/system/subagent-system-prompt.md +18 -9
- package/src/prompts/system/subagent-user-prompt.md +1 -10
- package/src/prompts/system/system-prompt.md +159 -232
- package/src/prompts/tools/ask.md +0 -1
- package/src/prompts/tools/bash.md +0 -34
- package/src/prompts/tools/eval.md +27 -16
- package/src/prompts/tools/github.md +6 -5
- package/src/prompts/tools/hashline.md +1 -0
- package/src/prompts/tools/job.md +14 -6
- package/src/prompts/tools/task.md +20 -3
- package/src/registry/agent-registry.ts +2 -1
- package/src/sdk.ts +87 -89
- package/src/session/agent-session.ts +107 -37
- package/src/session/artifacts.ts +7 -4
- package/src/session/session-manager.ts +30 -1
- package/src/ssh/connection-manager.ts +32 -16
- package/src/ssh/sshfs-mount.ts +10 -7
- package/src/system-prompt.ts +3 -9
- package/src/task/executor.ts +23 -7
- package/src/task/index.ts +57 -36
- package/src/tool-discovery/tool-index.ts +21 -8
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +3 -2
- package/src/tools/bash.ts +30 -50
- package/src/tools/browser/tab-supervisor.ts +12 -2
- package/src/tools/eval.ts +59 -44
- package/src/tools/fetch.ts +1 -1
- package/src/tools/gh.ts +140 -4
- package/src/tools/index.ts +12 -11
- package/src/tools/job.ts +48 -12
- package/src/tools/path-utils.ts +21 -1
- package/src/tools/read.ts +74 -31
- package/src/tools/search.ts +16 -3
- package/src/tools/todo-write.ts +1 -1
- package/src/utils/file-display-mode.ts +11 -5
- package/src/web/scrapers/mastodon.ts +1 -1
- package/src/web/scrapers/repology.ts +7 -7
- package/src/internal-urls/jobs-protocol.ts +0 -119
- package/src/task/template.ts +0 -47
- package/src/tools/bash-normalize.ts +0 -107
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
} from "@oh-my-pi/pi-ai";
|
|
56
56
|
import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
57
57
|
import { abortableSleep, getAgentDbPath, isEnoent, logger, prompt, Snowflake } from "@oh-my-pi/pi-utils";
|
|
58
|
-
import type
|
|
58
|
+
import { type AsyncJob, AsyncJobManager } from "../async";
|
|
59
59
|
import type { Rule } from "../capability/rule";
|
|
60
60
|
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
61
61
|
import {
|
|
@@ -225,8 +225,6 @@ export interface AgentSessionConfig {
|
|
|
225
225
|
agent: Agent;
|
|
226
226
|
sessionManager: SessionManager;
|
|
227
227
|
settings: Settings;
|
|
228
|
-
/** Async background jobs launched by tools */
|
|
229
|
-
asyncJobManager?: AsyncJobManager;
|
|
230
228
|
/** Models to cycle through with Ctrl+P (from --models flag) */
|
|
231
229
|
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
232
230
|
/** Initial session thinking selector. */
|
|
@@ -285,6 +283,12 @@ export interface AgentSessionConfig {
|
|
|
285
283
|
obfuscator?: SecretObfuscator;
|
|
286
284
|
/** Logical owner for retained Python kernels created by this session. */
|
|
287
285
|
evalKernelOwnerId?: string;
|
|
286
|
+
/**
|
|
287
|
+
* AsyncJobManager that this session installed as the process-global instance.
|
|
288
|
+
* Only set for top-level sessions; subagents inherit the parent's manager and
|
|
289
|
+
* **MUST NOT** dispose it on their own teardown.
|
|
290
|
+
*/
|
|
291
|
+
ownedAsyncJobManager?: AsyncJobManager;
|
|
288
292
|
/** Agent identity (registry id like "0-Main" or "3-Alice") used for IRC routing. */
|
|
289
293
|
agentId?: string;
|
|
290
294
|
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
|
@@ -507,7 +511,6 @@ export class AgentSession {
|
|
|
507
511
|
|
|
508
512
|
readonly configWarnings: string[] = [];
|
|
509
513
|
|
|
510
|
-
#asyncJobManager: AsyncJobManager | undefined = undefined;
|
|
511
514
|
#scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
512
515
|
#thinkingLevel: ThinkingLevel | undefined;
|
|
513
516
|
#promptTemplates: PromptTemplate[];
|
|
@@ -558,6 +561,11 @@ export class AgentSession {
|
|
|
558
561
|
// Python execution state
|
|
559
562
|
#evalAbortControllers = new Set<AbortController>();
|
|
560
563
|
#evalKernelOwnerId: string;
|
|
564
|
+
/**
|
|
565
|
+
* AsyncJobManager owned by this session (top-level only). Subagents leave
|
|
566
|
+
* this undefined and **MUST NOT** dispose the global instance on teardown.
|
|
567
|
+
*/
|
|
568
|
+
readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
|
|
561
569
|
#pendingPythonMessages: PythonExecutionMessage[] = [];
|
|
562
570
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
563
571
|
#evalExecutionDisposing = false;
|
|
@@ -570,6 +578,7 @@ export class AgentSession {
|
|
|
570
578
|
#agentId: string | undefined;
|
|
571
579
|
#agentRegistry: AgentRegistry | undefined;
|
|
572
580
|
#providerSessionId: string | undefined;
|
|
581
|
+
#isDisposed = false;
|
|
573
582
|
// Extension system
|
|
574
583
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
575
584
|
#turnIndex = 0;
|
|
@@ -646,23 +655,32 @@ export class AgentSession {
|
|
|
646
655
|
#hindsightSessionState: HindsightSessionState | undefined = undefined;
|
|
647
656
|
readonly rawSseDebugBuffer: RawSseDebugBuffer;
|
|
648
657
|
|
|
649
|
-
#
|
|
650
|
-
if (process.platform !== "darwin")
|
|
651
|
-
|
|
652
|
-
|
|
658
|
+
#acquirePowerAssertion(): void {
|
|
659
|
+
if (process.platform !== "darwin") return;
|
|
660
|
+
if (this.#powerAssertion) return;
|
|
661
|
+
const idle = this.settings.get("power.preventIdleSleep");
|
|
662
|
+
const system = this.settings.get("power.preventSystemSleep");
|
|
663
|
+
const user = this.settings.get("power.declareUserActive");
|
|
664
|
+
const display = this.settings.get("power.preventDisplaySleep");
|
|
665
|
+
// All four off → user opted out; do nothing.
|
|
666
|
+
if (!idle && !system && !user && !display) return;
|
|
653
667
|
try {
|
|
654
|
-
this.#powerAssertion = MacOSPowerAssertion.start({
|
|
668
|
+
this.#powerAssertion = MacOSPowerAssertion.start({
|
|
669
|
+
reason: "Oh My Pi agent session",
|
|
670
|
+
idle,
|
|
671
|
+
system,
|
|
672
|
+
user,
|
|
673
|
+
display,
|
|
674
|
+
});
|
|
655
675
|
} catch (error) {
|
|
656
676
|
logger.warn("Failed to acquire macOS power assertion", { error: String(error) });
|
|
657
677
|
}
|
|
658
678
|
}
|
|
659
679
|
|
|
660
|
-
#
|
|
680
|
+
#releasePowerAssertion(): void {
|
|
661
681
|
const assertion = this.#powerAssertion;
|
|
662
682
|
this.#powerAssertion = undefined;
|
|
663
|
-
if (!assertion)
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
683
|
+
if (!assertion) return;
|
|
666
684
|
try {
|
|
667
685
|
assertion.stop();
|
|
668
686
|
} catch (error) {
|
|
@@ -670,13 +688,32 @@ export class AgentSession {
|
|
|
670
688
|
}
|
|
671
689
|
}
|
|
672
690
|
|
|
691
|
+
#beginInFlight(): void {
|
|
692
|
+
this.#promptInFlightCount++;
|
|
693
|
+
if (this.#promptInFlightCount === 1) {
|
|
694
|
+
this.#acquirePowerAssertion();
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
#endInFlight(): void {
|
|
699
|
+
this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
|
|
700
|
+
if (this.#promptInFlightCount === 0) {
|
|
701
|
+
this.#releasePowerAssertion();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
#resetInFlight(): void {
|
|
706
|
+
this.#promptInFlightCount = 0;
|
|
707
|
+
this.#releasePowerAssertion();
|
|
708
|
+
}
|
|
709
|
+
|
|
673
710
|
constructor(config: AgentSessionConfig) {
|
|
674
711
|
this.agent = config.agent;
|
|
675
712
|
this.sessionManager = config.sessionManager;
|
|
676
713
|
this.settings = config.settings;
|
|
677
|
-
|
|
678
|
-
this.#asyncJobManager = config.asyncJobManager;
|
|
714
|
+
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
679
715
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
716
|
+
this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
|
|
680
717
|
this.#scopedModels = config.scopedModels ?? [];
|
|
681
718
|
this.#thinkingLevel = config.thinkingLevel;
|
|
682
719
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
@@ -819,15 +856,16 @@ export class AgentSession {
|
|
|
819
856
|
}
|
|
820
857
|
|
|
821
858
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
822
|
-
|
|
823
|
-
|
|
859
|
+
const manager = AsyncJobManager.instance();
|
|
860
|
+
if (!manager) return null;
|
|
861
|
+
const running = manager.getRunningJobs().map(job => ({
|
|
824
862
|
id: job.id,
|
|
825
863
|
type: job.type,
|
|
826
864
|
status: job.status,
|
|
827
865
|
label: job.label,
|
|
828
866
|
startTime: job.startTime,
|
|
829
867
|
}));
|
|
830
|
-
const recent =
|
|
868
|
+
const recent = manager.getRecentJobs(options?.recentLimit ?? 5).map(job => ({
|
|
831
869
|
id: job.id,
|
|
832
870
|
type: job.type,
|
|
833
871
|
status: job.status,
|
|
@@ -837,6 +875,17 @@ export class AgentSession {
|
|
|
837
875
|
return { running, recent };
|
|
838
876
|
}
|
|
839
877
|
|
|
878
|
+
/**
|
|
879
|
+
* Cancel async jobs registered by *this* agent only. Used by lifecycle
|
|
880
|
+
* transitions (newSession, switchSession, handoff, dispose) so a subagent
|
|
881
|
+
* cleans up its own background work without touching its parent's jobs.
|
|
882
|
+
* No-op when no manager is installed or this session has no agent id.
|
|
883
|
+
*/
|
|
884
|
+
#cancelOwnAsyncJobs(): void {
|
|
885
|
+
if (!this.#agentId) return;
|
|
886
|
+
AsyncJobManager.instance()?.cancelAll({ ownerId: this.#agentId });
|
|
887
|
+
}
|
|
888
|
+
|
|
840
889
|
// =========================================================================
|
|
841
890
|
// Event Subscription
|
|
842
891
|
// =========================================================================
|
|
@@ -1710,7 +1759,6 @@ export class AgentSession {
|
|
|
1710
1759
|
}
|
|
1711
1760
|
|
|
1712
1761
|
#preCacheStreamingEditFile(event: AgentEvent): void {
|
|
1713
|
-
if (!this.settings.get("edit.streamingAbort")) return;
|
|
1714
1762
|
if (this.#streamingEditAbortTriggered) return;
|
|
1715
1763
|
if (event.type !== "message_update") return;
|
|
1716
1764
|
|
|
@@ -1726,6 +1774,9 @@ export class AgentSession {
|
|
|
1726
1774
|
const streamingEdit = this.#getStreamingEditToolCall(event);
|
|
1727
1775
|
if (!streamingEdit) return;
|
|
1728
1776
|
|
|
1777
|
+
// The auto-generated guard runs unconditionally: editing a generated file
|
|
1778
|
+
// is never the user's intent, and the cost of a false-positive abort is one
|
|
1779
|
+
// wasted turn vs. silently corrupting a regenerated source.
|
|
1729
1780
|
const shouldCheckAutoGenerated =
|
|
1730
1781
|
!streamingEdit.toolCall.id || !this.#streamingEditPrecheckedToolCallIds.has(streamingEdit.toolCall.id);
|
|
1731
1782
|
if (shouldCheckAutoGenerated) {
|
|
@@ -1739,7 +1790,12 @@ export class AgentSession {
|
|
|
1739
1790
|
);
|
|
1740
1791
|
}
|
|
1741
1792
|
|
|
1742
|
-
|
|
1793
|
+
// File-cache priming feeds #maybeAbortStreamingEdit's removed-lines check,
|
|
1794
|
+
// which is the optional patch-preview verification gated by
|
|
1795
|
+
// edit.streamingAbort. Skip the read when the setting is off.
|
|
1796
|
+
if (this.settings.get("edit.streamingAbort")) {
|
|
1797
|
+
this.#ensureFileCache(streamingEdit.resolvedPath);
|
|
1798
|
+
}
|
|
1743
1799
|
}
|
|
1744
1800
|
|
|
1745
1801
|
#ensureFileCache(resolvedPath: string): void {
|
|
@@ -2108,6 +2164,8 @@ export class AgentSession {
|
|
|
2108
2164
|
* Call this when completely done with the session.
|
|
2109
2165
|
*/
|
|
2110
2166
|
async dispose(): Promise<void> {
|
|
2167
|
+
this.#isDisposed = true;
|
|
2168
|
+
this.#pendingBackgroundExchanges = [];
|
|
2111
2169
|
this.#evalExecutionDisposing = true;
|
|
2112
2170
|
try {
|
|
2113
2171
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
@@ -2118,10 +2176,21 @@ export class AgentSession {
|
|
|
2118
2176
|
}
|
|
2119
2177
|
await this.#cancelPostPromptTasks();
|
|
2120
2178
|
this.#clearTodoClearTimers();
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2179
|
+
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
2180
|
+
// leak its background bash/task work into the parent's manager. Only
|
|
2181
|
+
// the session that owns the manager goes on to dispose it (which itself
|
|
2182
|
+
// nukes any leftover jobs and pending deliveries).
|
|
2183
|
+
this.#cancelOwnAsyncJobs();
|
|
2184
|
+
const ownedAsyncManager = this.#ownedAsyncJobManager;
|
|
2185
|
+
if (ownedAsyncManager) {
|
|
2186
|
+
const drained = await ownedAsyncManager.dispose({ timeoutMs: 3_000 });
|
|
2187
|
+
const deliveryState = ownedAsyncManager.getDeliveryState();
|
|
2188
|
+
if (drained === false && deliveryState) {
|
|
2189
|
+
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
|
2190
|
+
}
|
|
2191
|
+
if (AsyncJobManager.instance() === ownedAsyncManager) {
|
|
2192
|
+
AsyncJobManager.setInstance(undefined);
|
|
2193
|
+
}
|
|
2125
2194
|
}
|
|
2126
2195
|
const pythonExecutionsSettled = await this.#prepareEvalExecutionsForDispose();
|
|
2127
2196
|
if (!pythonExecutionsSettled) {
|
|
@@ -2130,7 +2199,7 @@ export class AgentSession {
|
|
|
2130
2199
|
);
|
|
2131
2200
|
}
|
|
2132
2201
|
await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
2133
|
-
this.#
|
|
2202
|
+
this.#releasePowerAssertion();
|
|
2134
2203
|
await this.sessionManager.close();
|
|
2135
2204
|
this.#closeAllProviderSessions("dispose");
|
|
2136
2205
|
const hindsightState = this.setHindsightSessionState(undefined);
|
|
@@ -3171,7 +3240,7 @@ export class AgentSession {
|
|
|
3171
3240
|
skipPostPromptRecoveryWait?: boolean;
|
|
3172
3241
|
},
|
|
3173
3242
|
): Promise<void> {
|
|
3174
|
-
this.#
|
|
3243
|
+
this.#beginInFlight();
|
|
3175
3244
|
const generation = this.#promptGeneration;
|
|
3176
3245
|
try {
|
|
3177
3246
|
// Flush any pending bash messages before the new prompt
|
|
@@ -3291,7 +3360,7 @@ export class AgentSession {
|
|
|
3291
3360
|
await this.#waitForPostPromptRecovery();
|
|
3292
3361
|
}
|
|
3293
3362
|
} finally {
|
|
3294
|
-
this.#
|
|
3363
|
+
this.#endInFlight();
|
|
3295
3364
|
}
|
|
3296
3365
|
}
|
|
3297
3366
|
|
|
@@ -3877,7 +3946,7 @@ export class AgentSession {
|
|
|
3877
3946
|
// Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
|
|
3878
3947
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
3879
3948
|
// a subsequent prompt() can incorrectly observe the session as busy after an abort.
|
|
3880
|
-
this.#
|
|
3949
|
+
this.#resetInFlight();
|
|
3881
3950
|
// Safety net: if the agent loop aborted without producing an assistant
|
|
3882
3951
|
// message (e.g. failed before the first stream), the in-flight yield was
|
|
3883
3952
|
// never resolved or rejected by the normal message_end path. Reject it now
|
|
@@ -3917,7 +3986,7 @@ export class AgentSession {
|
|
|
3917
3986
|
|
|
3918
3987
|
this.#disconnectFromAgent();
|
|
3919
3988
|
await this.abort();
|
|
3920
|
-
this.#
|
|
3989
|
+
this.#cancelOwnAsyncJobs();
|
|
3921
3990
|
this.#closeAllProviderSessions("new session");
|
|
3922
3991
|
this.agent.reset();
|
|
3923
3992
|
if (options?.drop && previousSessionFile) {
|
|
@@ -4699,7 +4768,7 @@ export class AgentSession {
|
|
|
4699
4768
|
if (handoffSignal.aborted) {
|
|
4700
4769
|
throw new Error("Handoff cancelled");
|
|
4701
4770
|
}
|
|
4702
|
-
this.#
|
|
4771
|
+
this.#beginInFlight();
|
|
4703
4772
|
try {
|
|
4704
4773
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
4705
4774
|
await this.#promptAgentWithIdleRetry([
|
|
@@ -4711,7 +4780,7 @@ export class AgentSession {
|
|
|
4711
4780
|
},
|
|
4712
4781
|
]);
|
|
4713
4782
|
} finally {
|
|
4714
|
-
this.#
|
|
4783
|
+
this.#endInFlight();
|
|
4715
4784
|
}
|
|
4716
4785
|
await completionPromise;
|
|
4717
4786
|
|
|
@@ -4725,7 +4794,7 @@ export class AgentSession {
|
|
|
4725
4794
|
// Start a new session
|
|
4726
4795
|
const previousSessionFile = this.sessionFile;
|
|
4727
4796
|
await this.sessionManager.flush();
|
|
4728
|
-
this.#
|
|
4797
|
+
this.#cancelOwnAsyncJobs();
|
|
4729
4798
|
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
|
4730
4799
|
this.agent.reset();
|
|
4731
4800
|
this.#syncAgentSessionId();
|
|
@@ -6644,7 +6713,7 @@ export class AgentSession {
|
|
|
6644
6713
|
const incomingRecord: CustomMessage = {
|
|
6645
6714
|
role: "custom",
|
|
6646
6715
|
customType: "irc:incoming",
|
|
6647
|
-
content: `[IRC \`${args.from}\`
|
|
6716
|
+
content: `[IRC \`${args.from}\` → you]\n\n${args.message}`,
|
|
6648
6717
|
display: true,
|
|
6649
6718
|
details: { from: args.from, message: args.message },
|
|
6650
6719
|
attribution: "agent",
|
|
@@ -6676,7 +6745,7 @@ export class AgentSession {
|
|
|
6676
6745
|
const replyRecord: CustomMessage = {
|
|
6677
6746
|
role: "custom",
|
|
6678
6747
|
customType: "irc:autoreply",
|
|
6679
|
-
content: `[IRC you
|
|
6748
|
+
content: `[IRC you → \`${args.from}\` (auto)]\n\n${replyText}`,
|
|
6680
6749
|
display: true,
|
|
6681
6750
|
details: { to: args.from, reply: replyText },
|
|
6682
6751
|
attribution: "agent",
|
|
@@ -6715,7 +6784,7 @@ export class AgentSession {
|
|
|
6715
6784
|
const mainRef = registry.get(MAIN_AGENT_ID);
|
|
6716
6785
|
const mainSession = mainRef?.session;
|
|
6717
6786
|
if (!mainSession || mainSession === this) return;
|
|
6718
|
-
const arrow = args.kind === "reply" ? "
|
|
6787
|
+
const arrow = args.kind === "reply" ? "→ (auto)" : "→";
|
|
6719
6788
|
const relayRecord: CustomMessage = {
|
|
6720
6789
|
role: "custom",
|
|
6721
6790
|
customType: "irc:relay",
|
|
@@ -6853,7 +6922,8 @@ export class AgentSession {
|
|
|
6853
6922
|
if (this.#scheduledBackgroundExchangeFlush) return;
|
|
6854
6923
|
this.#scheduledBackgroundExchangeFlush = true;
|
|
6855
6924
|
const attempt = (): void => {
|
|
6856
|
-
if (this.#pendingBackgroundExchanges.length === 0) {
|
|
6925
|
+
if (this.#pendingBackgroundExchanges.length === 0 || this.#isDisposed) {
|
|
6926
|
+
this.#pendingBackgroundExchanges = [];
|
|
6857
6927
|
this.#scheduledBackgroundExchangeFlush = false;
|
|
6858
6928
|
return;
|
|
6859
6929
|
}
|
|
@@ -7117,7 +7187,7 @@ export class AgentSession {
|
|
|
7117
7187
|
|
|
7118
7188
|
// Flush pending writes before branching
|
|
7119
7189
|
await this.sessionManager.flush();
|
|
7120
|
-
this.#
|
|
7190
|
+
this.#cancelOwnAsyncJobs();
|
|
7121
7191
|
|
|
7122
7192
|
if (!selectedEntry.parentId) {
|
|
7123
7193
|
await this.sessionManager.newSession({ parentSession: previousSessionFile });
|
package/src/session/artifacts.ts
CHANGED
|
@@ -12,6 +12,10 @@ import * as path from "node:path";
|
|
|
12
12
|
*
|
|
13
13
|
* Artifacts are stored with sequential IDs in the session's artifact directory.
|
|
14
14
|
* The directory is created lazily on first write.
|
|
15
|
+
*
|
|
16
|
+
* Subagents do not own their own `ArtifactManager`. The parent's instance is
|
|
17
|
+
* adopted via `SessionManager.adoptArtifactManager`, so the whole parent +
|
|
18
|
+
* subagent tree shares one ID space and one directory.
|
|
15
19
|
*/
|
|
16
20
|
export class ArtifactManager {
|
|
17
21
|
#nextId = 0;
|
|
@@ -20,11 +24,10 @@ export class ArtifactManager {
|
|
|
20
24
|
#initialized = false;
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
|
-
* @param
|
|
27
|
+
* @param dir Directory that will hold artifact files. Created lazily on first save.
|
|
24
28
|
*/
|
|
25
|
-
constructor(
|
|
26
|
-
|
|
27
|
-
this.#dir = sessionFile.slice(0, -6);
|
|
29
|
+
constructor(dir: string) {
|
|
30
|
+
this.#dir = dir;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -275,6 +275,7 @@ export type ReadonlySessionManager = Pick<
|
|
|
275
275
|
| "getSessionFile"
|
|
276
276
|
| "getSessionName"
|
|
277
277
|
| "getArtifactsDir"
|
|
278
|
+
| "getArtifactManager"
|
|
278
279
|
| "allocateArtifactPath"
|
|
279
280
|
| "saveArtifact"
|
|
280
281
|
| "getArtifactPath"
|
|
@@ -1622,6 +1623,10 @@ export class SessionManager {
|
|
|
1622
1623
|
#persistErrorReported = false;
|
|
1623
1624
|
#artifactManager: ArtifactManager | null = null;
|
|
1624
1625
|
#artifactManagerSessionFile: string | null = null;
|
|
1626
|
+
// When set, take precedence over the lazily-derived per-session manager.
|
|
1627
|
+
// Subagents adopt the parent's manager so artifact IDs are unique across the
|
|
1628
|
+
// whole agent tree and all files land in the parent's artifacts dir.
|
|
1629
|
+
#adoptedArtifactManager: ArtifactManager | null = null;
|
|
1625
1630
|
// In-memory artifact fallback for non-persistent sessions (persist=false).
|
|
1626
1631
|
// Keyed by sequential numeric ID string; mirrors the file-based ArtifactManager ID scheme.
|
|
1627
1632
|
#inMemoryArtifacts: Map<string, string> | null = null;
|
|
@@ -1675,6 +1680,7 @@ export class SessionManager {
|
|
|
1675
1680
|
this.#persistErrorReported = false;
|
|
1676
1681
|
this.#artifactManager = null;
|
|
1677
1682
|
this.#artifactManagerSessionFile = null;
|
|
1683
|
+
this.#adoptedArtifactManager = null;
|
|
1678
1684
|
this.#buildIndex();
|
|
1679
1685
|
if (this.#sessionFile) {
|
|
1680
1686
|
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
@@ -2120,17 +2126,40 @@ export class SessionManager {
|
|
|
2120
2126
|
/**
|
|
2121
2127
|
* Returns the session artifacts directory path (session file path without .jsonl).
|
|
2122
2128
|
* Returns null when the session is not persisted to a file.
|
|
2129
|
+
* When this session has adopted an external ArtifactManager (subagent case),
|
|
2130
|
+
* returns that manager's directory so reads/writes land in the shared parent
|
|
2131
|
+
* dir instead of a private (non-existent) subdir.
|
|
2123
2132
|
*/
|
|
2124
2133
|
getArtifactsDir(): string | null {
|
|
2134
|
+
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager.dir;
|
|
2125
2135
|
const sessionFile = this.#sessionFile;
|
|
2126
2136
|
return sessionFile ? sessionFile.slice(0, -6) : null;
|
|
2127
2137
|
}
|
|
2128
2138
|
|
|
2139
|
+
/**
|
|
2140
|
+
* Adopt an externally-owned ArtifactManager. Used by subagents to share
|
|
2141
|
+
* the parent session's artifact directory and ID counter.
|
|
2142
|
+
*/
|
|
2143
|
+
adoptArtifactManager(manager: ArtifactManager): void {
|
|
2144
|
+
this.#adoptedArtifactManager = manager;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/**
|
|
2148
|
+
* Returns the ArtifactManager this session writes through. Lazily creates
|
|
2149
|
+
* one bound to the current session file unless an external manager was
|
|
2150
|
+
* adopted via `adoptArtifactManager`. Returns null only for non-persistent
|
|
2151
|
+
* sessions with no adopted manager.
|
|
2152
|
+
*/
|
|
2153
|
+
getArtifactManager(): ArtifactManager | null {
|
|
2154
|
+
return this.#getOrCreateArtifactManager();
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2129
2157
|
/**
|
|
2130
2158
|
* Returns an artifact manager bound to the current session file.
|
|
2131
2159
|
* Recreates the manager when the active session file changes.
|
|
2132
2160
|
*/
|
|
2133
2161
|
#getOrCreateArtifactManager(): ArtifactManager | null {
|
|
2162
|
+
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager;
|
|
2134
2163
|
const sessionFile = this.#sessionFile;
|
|
2135
2164
|
if (!sessionFile) {
|
|
2136
2165
|
this.#artifactManager = null;
|
|
@@ -2142,7 +2171,7 @@ export class SessionManager {
|
|
|
2142
2171
|
return this.#artifactManager;
|
|
2143
2172
|
}
|
|
2144
2173
|
|
|
2145
|
-
const manager = new ArtifactManager(sessionFile);
|
|
2174
|
+
const manager = new ArtifactManager(sessionFile.slice(0, -6));
|
|
2146
2175
|
this.#artifactManager = manager;
|
|
2147
2176
|
this.#artifactManagerSessionFile = sessionFile;
|
|
2148
2177
|
return manager;
|
|
@@ -15,6 +15,11 @@ export interface SSHConnectionTarget {
|
|
|
15
15
|
|
|
16
16
|
export type SSHHostOs = "windows" | "linux" | "macos" | "unknown";
|
|
17
17
|
export type SSHHostShell = "cmd" | "powershell" | "bash" | "zsh" | "sh" | "unknown";
|
|
18
|
+
export type SshPlatform = typeof process.platform;
|
|
19
|
+
|
|
20
|
+
export function supportsSshControlMaster(platform: SshPlatform = process.platform): boolean {
|
|
21
|
+
return platform !== "win32";
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
export interface SSHHostInfo {
|
|
20
25
|
version: number;
|
|
@@ -33,6 +38,10 @@ const activeHosts = new Map<string, SSHConnectionTarget>();
|
|
|
33
38
|
const pendingConnections = new Map<string, Promise<void>>();
|
|
34
39
|
const hostInfoCache = new Map<string, SSHHostInfo>();
|
|
35
40
|
|
|
41
|
+
interface SSHArgsOptions {
|
|
42
|
+
platform?: SshPlatform;
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
function ensureControlDir() {
|
|
37
46
|
fs.mkdirSync(CONTROL_DIR, { recursive: true, mode: 0o700 });
|
|
38
47
|
try {
|
|
@@ -66,20 +75,14 @@ async function validateKeyPermissions(keyPath?: string): Promise<void> {
|
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
function buildCommonArgs(host: SSHConnectionTarget): string[] {
|
|
70
|
-
const args = [
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"ControlMaster=auto",
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"ControlPersist=3600",
|
|
78
|
-
"-o",
|
|
79
|
-
"BatchMode=yes",
|
|
80
|
-
"-o",
|
|
81
|
-
"StrictHostKeyChecking=accept-new",
|
|
82
|
-
];
|
|
78
|
+
function buildCommonArgs(host: SSHConnectionTarget, options?: SSHArgsOptions): string[] {
|
|
79
|
+
const args = ["-n"];
|
|
80
|
+
|
|
81
|
+
if (supportsSshControlMaster(options?.platform)) {
|
|
82
|
+
args.push("-o", "ControlMaster=auto", "-o", `ControlPath=${CONTROL_PATH}`, "-o", "ControlPersist=3600");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
args.push("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new");
|
|
83
86
|
|
|
84
87
|
if (host.port) {
|
|
85
88
|
args.push("-p", String(host.port));
|
|
@@ -357,9 +360,13 @@ export async function ensureHostInfo(host: SSHConnectionTarget): Promise<SSHHost
|
|
|
357
360
|
return probeHostInfo(host);
|
|
358
361
|
}
|
|
359
362
|
|
|
360
|
-
export async function buildRemoteCommand(
|
|
363
|
+
export async function buildRemoteCommand(
|
|
364
|
+
host: SSHConnectionTarget,
|
|
365
|
+
command: string,
|
|
366
|
+
options?: SSHArgsOptions,
|
|
367
|
+
): Promise<string[]> {
|
|
361
368
|
await validateKeyPermissions(host.keyPath);
|
|
362
|
-
return [...buildCommonArgs(host), buildSshTarget(host.username, host.host), command];
|
|
369
|
+
return [...buildCommonArgs(host, options), buildSshTarget(host.username, host.host), command];
|
|
363
370
|
}
|
|
364
371
|
|
|
365
372
|
let registered = false;
|
|
@@ -385,6 +392,14 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
|
|
|
385
392
|
}
|
|
386
393
|
|
|
387
394
|
const target = buildSshTarget(host.username, host.host);
|
|
395
|
+
if (!supportsSshControlMaster()) {
|
|
396
|
+
activeHosts.set(key, host);
|
|
397
|
+
if (!hostInfoCache.has(key) && !(await loadHostInfoFromDisk(host))) {
|
|
398
|
+
await probeHostInfo(host);
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
388
403
|
const check = await runSshSync(["-O", "check", ...buildCommonArgs(host), target]);
|
|
389
404
|
if (check.exitCode === 0) {
|
|
390
405
|
activeHosts.set(key, host);
|
|
@@ -415,6 +430,7 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
|
|
|
415
430
|
}
|
|
416
431
|
|
|
417
432
|
async function closeConnectionInternal(host: SSHConnectionTarget): Promise<void> {
|
|
433
|
+
if (!supportsSshControlMaster()) return;
|
|
418
434
|
const target = buildSshTarget(host.username, host.host);
|
|
419
435
|
await runSshSync(["-O", "exit", ...buildCommonArgs(host), target]);
|
|
420
436
|
}
|
package/src/ssh/sshfs-mount.ts
CHANGED
|
@@ -2,7 +2,12 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { $which, getRemoteDir, postmortem } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { $ } from "bun";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getControlDir,
|
|
7
|
+
getControlPathTemplate,
|
|
8
|
+
type SSHConnectionTarget,
|
|
9
|
+
supportsSshControlMaster,
|
|
10
|
+
} from "./connection-manager";
|
|
6
11
|
import { buildSshTarget, sanitizeHostName } from "./utils";
|
|
7
12
|
|
|
8
13
|
const REMOTE_DIR = getRemoteDir();
|
|
@@ -40,14 +45,12 @@ function buildSshfsArgs(host: SSHConnectionTarget): string[] {
|
|
|
40
45
|
"BatchMode=yes",
|
|
41
46
|
"-o",
|
|
42
47
|
"StrictHostKeyChecking=accept-new",
|
|
43
|
-
"-o",
|
|
44
|
-
"ControlMaster=auto",
|
|
45
|
-
"-o",
|
|
46
|
-
`ControlPath=${CONTROL_PATH}`,
|
|
47
|
-
"-o",
|
|
48
|
-
"ControlPersist=3600",
|
|
49
48
|
];
|
|
50
49
|
|
|
50
|
+
if (supportsSshControlMaster()) {
|
|
51
|
+
args.push("-o", "ControlMaster=auto", "-o", `ControlPath=${CONTROL_PATH}`, "-o", "ControlPersist=3600");
|
|
52
|
+
}
|
|
53
|
+
|
|
51
54
|
if (host.port) {
|
|
52
55
|
args.push("-p", String(host.port));
|
|
53
56
|
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { loadSkills, type Skill } from "./extensibility/skills";
|
|
|
14
14
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
15
15
|
import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
|
|
16
16
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
17
|
+
import { shortenPath } from "./tools/render-utils";
|
|
17
18
|
import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
18
19
|
|
|
19
20
|
interface AlwaysApplyRule {
|
|
@@ -503,7 +504,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
503
504
|
|
|
504
505
|
const date = new Date().toISOString().slice(0, 10);
|
|
505
506
|
const dateTime = date;
|
|
506
|
-
const promptCwd = resolvedCwd.replace(/\\/g, "/");
|
|
507
|
+
const promptCwd = shortenPath(resolvedCwd.replace(/\\/g, "/"));
|
|
507
508
|
|
|
508
509
|
// Build tool metadata for system prompt rendering
|
|
509
510
|
// Priority: explicit list > tools map > defaults
|
|
@@ -541,7 +542,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
541
542
|
const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
|
|
542
543
|
|
|
543
544
|
const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
|
|
544
|
-
const reportToolIssueToolName = toolPromptNames.get("report_tool_issue") ?? "report_tool_issue";
|
|
545
545
|
const data = {
|
|
546
546
|
systemPromptCustomization: effectiveSystemPromptCustomization,
|
|
547
547
|
customPrompt: resolvedCustomPrompt,
|
|
@@ -568,13 +568,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
568
568
|
eagerTasks,
|
|
569
569
|
secretsEnabled,
|
|
570
570
|
};
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
// When autoqa is active the report_tool_issue tool is in the tool set — nudge the agent.
|
|
574
|
-
if (toolNames.includes("report_tool_issue")) {
|
|
575
|
-
rendered += `\n\n<critical>\nThe \`${reportToolIssueToolName}\` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call \`${reportToolIssueToolName}\` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>`;
|
|
576
|
-
}
|
|
577
|
-
|
|
571
|
+
const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
|
|
578
572
|
const systemPrompt = [rendered];
|
|
579
573
|
const projectPrompt = resolvedCustomPrompt ? "" : prompt.render(projectPromptTemplate, data).trim();
|
|
580
574
|
if (projectPrompt) {
|