@oh-my-pi/pi-coding-agent 15.2.3 → 15.3.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 +41 -0
- package/dist/types/config/settings-schema.d.ts +34 -1
- package/dist/types/config/settings.d.ts +6 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/goals/runtime.d.ts +4 -0
- package/dist/types/hashline/constants.d.ts +0 -2
- package/dist/types/hashline/hash.d.ts +13 -39
- package/dist/types/hashline/parser.d.ts +2 -6
- package/dist/types/modes/components/status-line/types.d.ts +10 -0
- package/dist/types/modes/components/status-line.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/shared.d.ts +9 -0
- package/dist/types/modes/theme/shimmer.d.ts +6 -3
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +17 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/task/types.d.ts +35 -0
- package/dist/types/tools/bash-command-fixup.d.ts +0 -5
- package/dist/types/utils/clipboard.d.ts +3 -1
- package/dist/types/utils/image-resize.d.ts +4 -1
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +1 -8
- package/src/config/settings-schema.ts +29 -1
- package/src/config/settings.ts +19 -0
- package/src/discovery/helpers.ts +5 -1
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +5 -7
- package/src/edit/streaming.ts +24 -12
- package/src/extensibility/plugins/legacy-pi-compat.ts +27 -5
- package/src/goals/runtime.ts +35 -13
- package/src/hashline/constants.ts +0 -3
- package/src/hashline/diff.ts +1 -1
- package/src/hashline/execute.ts +2 -2
- package/src/hashline/grammar.lark +7 -8
- package/src/hashline/hash.ts +21 -43
- package/src/hashline/input.ts +15 -13
- package/src/hashline/parser.ts +62 -161
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +1 -1
- package/src/modes/components/model-selector.ts +53 -22
- package/src/modes/components/status-line/segments.ts +53 -0
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +147 -12
- package/src/modes/controllers/command-controller.ts +9 -0
- package/src/modes/controllers/event-controller.ts +10 -1
- package/src/modes/interactive-mode.ts +74 -18
- package/src/modes/shared.ts +16 -0
- package/src/modes/theme/shimmer.ts +15 -6
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/context-usage.ts +25 -2
- package/src/modes/utils/ui-helpers.ts +11 -1
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/tools/hashline.md +62 -81
- package/src/sdk.ts +24 -0
- package/src/session/agent-session.ts +58 -0
- package/src/session/session-manager.ts +54 -1
- package/src/slash-commands/builtin-registry.ts +10 -0
- package/src/task/executor.ts +50 -1
- package/src/task/index.ts +11 -0
- package/src/task/render.ts +26 -2
- package/src/task/types.ts +35 -0
- package/src/tools/bash-command-fixup.ts +0 -10
- package/src/tools/bash.ts +1 -9
- package/src/utils/clipboard.ts +68 -3
- package/src/utils/commit-message-generator.ts +6 -1
- package/src/utils/image-resize.ts +51 -26
- package/src/utils/title-generator.ts +45 -13
- package/dist/types/modes/components/status-line-segment-editor.d.ts +0 -24
- package/src/modes/components/status-line-segment-editor.ts +0 -359
|
@@ -6,5 +6,6 @@ description: {{jsonStringify description}}
|
|
|
6
6
|
{{/if}}{{#if model}}model: {{jsonStringify model}}
|
|
7
7
|
{{/if}}{{#if thinkingLevel}}thinking-level: {{jsonStringify thinkingLevel}}
|
|
8
8
|
{{/if}}{{#if blocking}}blocking: true
|
|
9
|
+
{{/if}}{{#if autoloadSkills}}autoloadSkills: {{jsonStringify autoloadSkills}}
|
|
9
10
|
{{/if}}---
|
|
10
11
|
{{body}}
|
|
@@ -1,58 +1,36 @@
|
|
|
1
1
|
Your patch language is a compact, line-anchored edit format.
|
|
2
2
|
|
|
3
|
-
A patch contains one or more file sections. The first non-blank line of every edit section MUST be
|
|
3
|
+
A patch contains one or more file sections. The first non-blank line of every edit section MUST be `§PATH`.
|
|
4
4
|
Operations reference lines in the file by their line number and hash, called "Anchors", e.g. `5th`, `123ab`.
|
|
5
5
|
You MUST copy them verbatim from the latest output for the file you're editing.
|
|
6
6
|
|
|
7
7
|
Purely textual format. The tool has NO awareness of language, indentation, brackets, fences, or table widths. You MUST emit valid syntax in replacements/insertions.
|
|
8
8
|
|
|
9
9
|
<ops>
|
|
10
|
-
|
|
10
|
+
§PATH header: subsequent ops apply to PATH
|
|
11
11
|
Each op line is ONE of:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
»ANCHOR insert lines AFTER the anchored line (or EOF); payload follows on subsequent lines
|
|
13
|
+
«ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows on subsequent lines
|
|
14
|
+
≔A..B replace the inclusive range A..B with payload; delete the range if no payload follows
|
|
15
|
+
≔A shorthand for ≔A..A
|
|
16
16
|
</ops>
|
|
17
17
|
|
|
18
|
-
<format-reminder>
|
|
19
|
-
Op lines carry no content — payload goes on the next line.
|
|
20
|
-
|
|
21
|
-
WRONG: + 5pg| some code
|
|
22
|
-
WRONG: {{hsep}} some code
|
|
23
|
-
RIGHT: + 5pg
|
|
24
|
-
{{hsep}}some code
|
|
25
|
-
|
|
26
|
-
A single `+`/`<`/`=` op accepts MANY `{{hsep}}` payload lines. To insert N consecutive lines, write ONE op followed by N payload lines — NEVER N ops with one payload each.
|
|
27
|
-
|
|
28
|
-
WRONG (one op per inserted line, with fabricated anchors):
|
|
29
|
-
+ 5pg
|
|
30
|
-
{{hsep}}first new line
|
|
31
|
-
+ 6xx ← FABRICATED
|
|
32
|
-
{{hsep}}second new line
|
|
33
|
-
|
|
34
|
-
RIGHT (one op, many payload lines):
|
|
35
|
-
+ 5pg
|
|
36
|
-
{{hsep}}first new line
|
|
37
|
-
{{hsep}}second new line
|
|
38
|
-
</format-reminder>
|
|
39
|
-
|
|
40
18
|
<rules>
|
|
41
|
-
- Every payload line MUST start with `{{hsep}}` immediately followed by payload text. Do NOT add a readability space after `{{hsep}}`.
|
|
42
|
-
- Every character after `{{hsep}}` is file content. If the target line intentionally starts with one space, write exactly one space after `{{hsep}}`; otherwise write none.
|
|
43
19
|
- Payload text is verbatim — NEVER escape unicode.
|
|
20
|
+
- Payload ends at the next `»`, `«`, `≔`, `§`, envelope marker, or EOF.
|
|
21
|
+
- `≔A..B` with no payload deletes the range. To keep a blank line, include one explicit empty payload line.
|
|
44
22
|
- **Payload is only what's NEW relative to your range:**
|
|
45
|
-
-
|
|
46
|
-
-
|
|
23
|
+
- `≔` replaces inside; NEVER include lines outside.
|
|
24
|
+
- `»`/`«` adds at the anchor; NEVER repeat line A or neighbors.
|
|
47
25
|
- Payload matching nearby content duplicates — drop it or widen.
|
|
48
26
|
- **Pick a self-contained unit first.** Touching a multiline construct? Widen to the whole thing.
|
|
49
|
-
- Then smallest op: add →
|
|
27
|
+
- Then smallest op: add → `»`/`«`; delete/replace → `≔`.
|
|
50
28
|
</rules>
|
|
51
29
|
|
|
52
30
|
<brace-shapes>
|
|
53
31
|
When braces bound your edit, you SHOULD prefer these shapes:
|
|
54
32
|
- **Whole block**: range spans `{` through matching `}`.
|
|
55
|
-
- **Signature only**: one-line
|
|
33
|
+
- **Signature only**: one-line `≔` on the opener; body untouched.
|
|
56
34
|
- **Insert inside**: anchor on `{` or last interior line; NEVER repeat the braces.
|
|
57
35
|
- **End on `}`**: only when that `}` is part of the change. Otherwise extend or stop earlier.
|
|
58
36
|
</brace-shapes>
|
|
@@ -61,9 +39,9 @@ When braces bound your edit, you SHOULD prefer these shapes:
|
|
|
61
39
|
- **NEVER replay past your range.** Stop before B+1; extend B if it must go.
|
|
62
40
|
- **NEVER duplicate chunks inside one payload.** Caught re-emitting? Rewrite.
|
|
63
41
|
- **Anchor only inside the visible region.** B+1 truncated? Re-`read` first.
|
|
64
|
-
- **You SHOULD prefer the narrowest self-contained edit.**
|
|
42
|
+
- **You SHOULD prefer the narrowest self-contained edit.** Narrow range beats wide range.
|
|
65
43
|
- **Anchors reference the file as last read.** NEVER shift for prior ops.
|
|
66
|
-
- **One
|
|
44
|
+
- **One `»`/`«` op per block, NOT per line.** N lines = ONE op, N payloads. Collapse adjacent ops.
|
|
67
45
|
- **NEVER fabricate anchor hashes.** Missing? Re-`read`.
|
|
68
46
|
</common-failures>
|
|
69
47
|
|
|
@@ -79,71 +57,74 @@ When braces bound your edit, you SHOULD prefer these shapes:
|
|
|
79
57
|
|
|
80
58
|
<examples>
|
|
81
59
|
# Replace one line (the payload must re-emit the original indentation)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
60
|
+
§mod.ts
|
|
61
|
+
≔{{hrefr 1}}
|
|
62
|
+
const TITLE = "Mrs";
|
|
85
63
|
|
|
86
64
|
# Replace a full multiline statement (widen to a self-contained boundary)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
65
|
+
§mod.ts
|
|
66
|
+
≔{{hrefr 3}}..{{hrefr 6}}
|
|
67
|
+
return [
|
|
68
|
+
"Mrs",
|
|
69
|
+
name?.trim() || "guest",
|
|
70
|
+
].join(" ");
|
|
93
71
|
|
|
94
72
|
# Insert AFTER/BEFORE a line
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
73
|
+
§mod.ts
|
|
74
|
+
»{{hrefr 4}}
|
|
75
|
+
"Dr",
|
|
76
|
+
«{{hrefr 5}}
|
|
77
|
+
"Dr",
|
|
100
78
|
|
|
101
79
|
# Append to file
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
80
|
+
§mod.ts
|
|
81
|
+
»EOF
|
|
82
|
+
export const done = true;
|
|
105
83
|
|
|
106
84
|
# Delete a line
|
|
107
|
-
|
|
108
|
-
|
|
85
|
+
§mod.ts
|
|
86
|
+
≔{{hrefr 5}}
|
|
87
|
+
|
|
88
|
+
# Blank a line (replace with LF: the empty payload is the blank line before `»EOF`)
|
|
89
|
+
§mod.ts
|
|
90
|
+
≔{{hrefr 5}}
|
|
109
91
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
= {{hrefr 5}}..{{hrefr 5}}
|
|
92
|
+
»EOF
|
|
93
|
+
export const done = true;
|
|
113
94
|
</examples>
|
|
114
95
|
|
|
115
96
|
<anti-pattern>
|
|
116
97
|
# WRONG — replaces 2 lines just to add one.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
98
|
+
§mod.ts
|
|
99
|
+
≔{{hrefr 1}}..{{hrefr 2}}
|
|
100
|
+
const TITLE = "Mr";
|
|
101
|
+
const DEBUG = false;
|
|
102
|
+
export function greet(name) {
|
|
122
103
|
# RIGHT — same effect, one-line insert
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
104
|
+
§mod.ts
|
|
105
|
+
»{{hrefr 1}}
|
|
106
|
+
const DEBUG = false;
|
|
126
107
|
|
|
127
108
|
# WRONG — replace from the middle of a larger statement (error-prone)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
109
|
+
§mod.ts
|
|
110
|
+
≔{{hrefr 4}}..{{hrefr 5}}
|
|
111
|
+
"Dr",
|
|
112
|
+
name?.trim() || "guest",
|
|
132
113
|
# RIGHT — widen to the full statement
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
114
|
+
§mod.ts
|
|
115
|
+
≔{{hrefr 3}}..{{hrefr 6}}
|
|
116
|
+
return [
|
|
117
|
+
"Dr",
|
|
118
|
+
name?.trim() || "guest",
|
|
119
|
+
].join(" ");
|
|
139
120
|
</anti-pattern>
|
|
140
121
|
|
|
141
122
|
<critical>
|
|
142
123
|
- Copy anchors verbatim (line number + 2-char hash); NEVER include the `|TEXT` body.
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
146
|
-
- Multiple ops are cheap. SHOULD prefer two narrow ops over one wide
|
|
147
|
-
- Before
|
|
124
|
+
- NEVER write unified diff syntax. Headers are `§PATH`; ops are `»`/`«`/`≔`.
|
|
125
|
+
- `≔A..B` deletes the range when no payload follows. To keep a blank line, include one explicit empty payload line.
|
|
126
|
+
- `≔A..B` with payload writes exactly that payload. Edge line matches just outside? Widen, or it duplicates.
|
|
127
|
+
- Multiple ops are cheap. SHOULD prefer two narrow ops over one wide `≔`.
|
|
128
|
+
- Before `≔A..B`, mentally delete A..B. Splits an unclosed bracket/brace/string from above, or orphans a closer inside? You're bisecting a construct.
|
|
148
129
|
- NEVER use this tool to reformat code (indentation, whitespace, line wrapping, style). Run the project's formatter instead.
|
|
149
130
|
</critical>
|
package/src/sdk.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AgentMessage,
|
|
5
5
|
type AgentTelemetryConfig,
|
|
6
6
|
type AgentTool,
|
|
7
|
+
AppendOnlyContextManager,
|
|
7
8
|
INTENT_FIELD,
|
|
8
9
|
type ThinkingLevel,
|
|
9
10
|
} from "@oh-my-pi/pi-agent-core";
|
|
@@ -589,6 +590,24 @@ function registerPythonCleanup(): void {
|
|
|
589
590
|
postmortem.register("python-cleanup", disposeAllKernelSessions);
|
|
590
591
|
}
|
|
591
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Resolve whether to enable append-only context mode based on the setting and provider.
|
|
595
|
+
*
|
|
596
|
+
* - `"on"` → always enable
|
|
597
|
+
* - `"off"` → never enable
|
|
598
|
+
* - `"auto"` → enable for DeepSeek (prefix-caching provider)
|
|
599
|
+
*/
|
|
600
|
+
function resolveAppendOnlyMode(setting: "auto" | "on" | "off" | undefined, provider: string): boolean {
|
|
601
|
+
switch (setting ?? "auto") {
|
|
602
|
+
case "on":
|
|
603
|
+
return true;
|
|
604
|
+
case "off":
|
|
605
|
+
return false;
|
|
606
|
+
default:
|
|
607
|
+
return provider === "deepseek";
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
592
611
|
function customToolToDefinition(tool: CustomTool): ToolDefinition {
|
|
593
612
|
const definition: ToolDefinition & { [TOOL_DEFINITION_MARKER]: true } = {
|
|
594
613
|
name: tool.name,
|
|
@@ -1897,6 +1916,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1897
1916
|
intentTracing: !!intentField,
|
|
1898
1917
|
getToolChoice: () => session?.nextToolChoice(),
|
|
1899
1918
|
telemetry: options.telemetry,
|
|
1919
|
+
appendOnlyContext: model
|
|
1920
|
+
? resolveAppendOnlyMode(settings.get("provider.appendOnlyContext"), model.provider)
|
|
1921
|
+
? new AppendOnlyContextManager()
|
|
1922
|
+
: undefined
|
|
1923
|
+
: undefined,
|
|
1900
1924
|
});
|
|
1901
1925
|
|
|
1902
1926
|
cursorEventEmitter = event => agent.emitExternalEvent(event);
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type AgentMessage,
|
|
27
27
|
type AgentState,
|
|
28
28
|
type AgentTool,
|
|
29
|
+
AppendOnlyContextManager,
|
|
29
30
|
resolveTelemetry,
|
|
30
31
|
ThinkingLevel,
|
|
31
32
|
} from "@oh-my-pi/pi-agent-core";
|
|
@@ -98,6 +99,7 @@ import {
|
|
|
98
99
|
} from "../config/model-resolver";
|
|
99
100
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
100
101
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
102
|
+
import { onAppendOnlyModeChanged } from "../config/settings";
|
|
101
103
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
102
104
|
import { loadCapability } from "../discovery";
|
|
103
105
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
@@ -1138,6 +1140,8 @@ export class AgentSession {
|
|
|
1138
1140
|
// Always subscribe to agent events for internal handling
|
|
1139
1141
|
// (session persistence, hooks, auto-compaction, retry logic)
|
|
1140
1142
|
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
|
1143
|
+
// Re-evaluate append-only context mode when the setting changes at runtime.
|
|
1144
|
+
onAppendOnlyModeChanged(_value => this.#syncAppendOnlyContext(this.model));
|
|
1141
1145
|
}
|
|
1142
1146
|
|
|
1143
1147
|
/** Model registry for API key resolution and model discovery */
|
|
@@ -3573,6 +3577,18 @@ export class AgentSession {
|
|
|
3573
3577
|
return this.#autoCompactionAbortController !== undefined || this.#compactionAbortController !== undefined;
|
|
3574
3578
|
}
|
|
3575
3579
|
|
|
3580
|
+
/**
|
|
3581
|
+
* Whether idle-flush tasks, auto-continuations, or other short-lived
|
|
3582
|
+
* post-prompt work are pending. True in the brief window after
|
|
3583
|
+
* `session.prompt()` returns but before a scheduled background delivery
|
|
3584
|
+
* (e.g. an async-job result) has finished its own streaming turn.
|
|
3585
|
+
* Loop-mode and similar auto-submit paths should treat this as a block
|
|
3586
|
+
* to avoid racing against the delivery turn.
|
|
3587
|
+
*/
|
|
3588
|
+
get hasPostPromptWork(): boolean {
|
|
3589
|
+
return this.#postPromptTasks.size > 0;
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3576
3592
|
/** All messages including custom types like BashExecutionMessage */
|
|
3577
3593
|
get messages(): AgentMessage[] {
|
|
3578
3594
|
return this.agent.state.messages;
|
|
@@ -5947,6 +5963,9 @@ export class AgentSession {
|
|
|
5947
5963
|
this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
|
5948
5964
|
}
|
|
5949
5965
|
this.agent.setModel(model);
|
|
5966
|
+
|
|
5967
|
+
// Re-evaluate append-only context mode — provider or setting may have changed
|
|
5968
|
+
this.#syncAppendOnlyContext(model);
|
|
5950
5969
|
}
|
|
5951
5970
|
|
|
5952
5971
|
#closeCodexProviderSessionsForHistoryRewrite(): void {
|
|
@@ -5955,6 +5974,24 @@ export class AgentSession {
|
|
|
5955
5974
|
this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
|
|
5956
5975
|
}
|
|
5957
5976
|
|
|
5977
|
+
/**
|
|
5978
|
+
* Re-evaluate append-only context mode, creating or destroying the
|
|
5979
|
+
* manager as needed. Called on model switch AND setting change.
|
|
5980
|
+
*/
|
|
5981
|
+
#syncAppendOnlyContext(model: Model | null | undefined): void {
|
|
5982
|
+
const setting = this.settings.get("provider.appendOnlyContext") ?? "auto";
|
|
5983
|
+
const enable = setting === "on" || (setting === "auto" && model?.provider === "deepseek");
|
|
5984
|
+
if (enable && !this.agent.appendOnlyContext) {
|
|
5985
|
+
this.agent.setAppendOnlyContext(new AppendOnlyContextManager());
|
|
5986
|
+
} else if (enable && this.agent.appendOnlyContext) {
|
|
5987
|
+
// Already active — invalidate prefix + log so the next turn
|
|
5988
|
+
// rebuilds for the current model's normalization.
|
|
5989
|
+
this.agent.appendOnlyContext.invalidateForModelChange();
|
|
5990
|
+
} else if (!enable && this.agent.appendOnlyContext) {
|
|
5991
|
+
this.agent.setAppendOnlyContext(undefined);
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
|
|
5958
5995
|
#closeProviderSessionsForModelSwitch(currentModel: Model, nextModel: Model): void {
|
|
5959
5996
|
const providerKeys = new Set<string>();
|
|
5960
5997
|
if (currentModel.api === "openai-codex-responses" || nextModel.api === "openai-codex-responses") {
|
|
@@ -7071,6 +7108,27 @@ export class AgentSession {
|
|
|
7071
7108
|
}
|
|
7072
7109
|
}
|
|
7073
7110
|
|
|
7111
|
+
// Fail-fast cap: if the provider asks us to wait longer than
|
|
7112
|
+
// retry.maxDelayMs and we have no fallback credential or model to
|
|
7113
|
+
// switch to, surface the error instead of sleeping. Defends against
|
|
7114
|
+
// 3-hour Anthropic rate-limit windows that would otherwise leave a
|
|
7115
|
+
// subagent (or interactive session) silently hung. The original
|
|
7116
|
+
// assistant error message is preserved in agent state so the caller
|
|
7117
|
+
// can act on it.
|
|
7118
|
+
const maxDelayMs = retrySettings.maxDelayMs;
|
|
7119
|
+
if (maxDelayMs > 0 && delayMs > maxDelayMs && !switchedCredential && !switchedModel) {
|
|
7120
|
+
const attempt = this.#retryAttempt;
|
|
7121
|
+
this.#retryAttempt = 0;
|
|
7122
|
+
await this.#emitSessionEvent({
|
|
7123
|
+
type: "auto_retry_end",
|
|
7124
|
+
success: false,
|
|
7125
|
+
attempt,
|
|
7126
|
+
finalError: `Provider requested ${delayMs}ms wait, exceeds retry.maxDelayMs (${maxDelayMs}ms). Original error: ${errorMessage}`,
|
|
7127
|
+
});
|
|
7128
|
+
this.#resolveRetry();
|
|
7129
|
+
return false;
|
|
7130
|
+
}
|
|
7131
|
+
|
|
7074
7132
|
await this.#emitSessionEvent({
|
|
7075
7133
|
type: "auto_retry_start",
|
|
7076
7134
|
attempt: this.#retryAttempt,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getProjectDir,
|
|
19
19
|
getSessionsDir,
|
|
20
20
|
getTerminalSessionsDir,
|
|
21
|
+
hasFsCode,
|
|
21
22
|
isEnoent,
|
|
22
23
|
logger,
|
|
23
24
|
parseJsonlLenient,
|
|
@@ -2146,7 +2147,59 @@ export class SessionManager {
|
|
|
2146
2147
|
{ ignoreError: true },
|
|
2147
2148
|
);
|
|
2148
2149
|
}
|
|
2150
|
+
// Windows can reject overwrite-style rename with EPERM even after our own writer is closed.
|
|
2151
|
+
// Move the old session file aside first so a failed retry can roll back to the last good file.
|
|
2149
2152
|
|
|
2153
|
+
async #replaceSessionFileAfterEperm(tempPath: string, targetPath: string, renameError: unknown): Promise<void> {
|
|
2154
|
+
const dir = path.resolve(targetPath, "..");
|
|
2155
|
+
const backupPath = path.join(dir, `.${path.basename(targetPath)}.${Snowflake.next()}.bak`);
|
|
2156
|
+
try {
|
|
2157
|
+
await this.storage.rename(targetPath, backupPath);
|
|
2158
|
+
} catch (err) {
|
|
2159
|
+
if (isEnoent(err)) {
|
|
2160
|
+
await this.storage.rename(tempPath, targetPath);
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
throw toError(renameError);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
try {
|
|
2167
|
+
await this.storage.rename(tempPath, targetPath);
|
|
2168
|
+
} catch (err) {
|
|
2169
|
+
const replaceError = toError(err);
|
|
2170
|
+
try {
|
|
2171
|
+
await this.storage.rename(backupPath, targetPath);
|
|
2172
|
+
} catch (rollbackErr) {
|
|
2173
|
+
const rollbackError = toError(rollbackErr);
|
|
2174
|
+
throw new Error(
|
|
2175
|
+
`Failed to replace session file after EPERM (${replaceError.message}); rollback from ${backupPath} also failed: ${rollbackError.message}`,
|
|
2176
|
+
{ cause: replaceError },
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
throw replaceError;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
try {
|
|
2183
|
+
await this.storage.unlink(backupPath);
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
if (!isEnoent(err)) {
|
|
2186
|
+
logger.warn("Failed to remove session rewrite backup", {
|
|
2187
|
+
sessionFile: targetPath,
|
|
2188
|
+
backupPath,
|
|
2189
|
+
error: toError(err).message,
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
async #replaceSessionFile(tempPath: string, targetPath: string): Promise<void> {
|
|
2196
|
+
try {
|
|
2197
|
+
await this.storage.rename(tempPath, targetPath);
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
if (!hasFsCode(err, "EPERM")) throw toError(err);
|
|
2200
|
+
await this.#replaceSessionFileAfterEperm(tempPath, targetPath, err);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2150
2203
|
async #writeEntriesAtomically(entries: FileEntry[]): Promise<void> {
|
|
2151
2204
|
if (!this.#sessionFile) return;
|
|
2152
2205
|
const dir = path.resolve(this.#sessionFile, "..");
|
|
@@ -2159,7 +2212,7 @@ export class SessionManager {
|
|
|
2159
2212
|
await writer.flush();
|
|
2160
2213
|
await writer.fsync();
|
|
2161
2214
|
await writer.close();
|
|
2162
|
-
await this
|
|
2215
|
+
await this.#replaceSessionFile(tempPath, this.#sessionFile);
|
|
2163
2216
|
} catch (err) {
|
|
2164
2217
|
try {
|
|
2165
2218
|
await writer.close();
|
|
@@ -72,7 +72,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
72
72
|
inlineHint: "[prompt]",
|
|
73
73
|
allowArgs: true,
|
|
74
74
|
handleTui: async (command, runtime) => {
|
|
75
|
+
const hadArgs = !!command.args;
|
|
75
76
|
await runtime.ctx.handlePlanModeCommand(command.args || undefined);
|
|
77
|
+
if (hadArgs && runtime.ctx.planModeEnabled) {
|
|
78
|
+
// plan was already active — preserve the typed command in input history
|
|
79
|
+
runtime.ctx.editor.addToHistory(command.text);
|
|
80
|
+
}
|
|
76
81
|
runtime.ctx.editor.setText("");
|
|
77
82
|
},
|
|
78
83
|
},
|
|
@@ -90,7 +95,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
90
95
|
inlineHint: "[objective]",
|
|
91
96
|
allowArgs: true,
|
|
92
97
|
handleTui: async (command, runtime) => {
|
|
98
|
+
const hadArgs = !!command.args;
|
|
93
99
|
await runtime.ctx.handleGoalModeCommand(command.args || undefined);
|
|
100
|
+
if (hadArgs && runtime.ctx.goalModeEnabled) {
|
|
101
|
+
// goal was already active — preserve the typed command in input history
|
|
102
|
+
runtime.ctx.editor.addToHistory(command.text);
|
|
103
|
+
}
|
|
94
104
|
runtime.ctx.editor.setText("");
|
|
95
105
|
},
|
|
96
106
|
},
|
package/src/task/executor.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
|
17
17
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
18
18
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
19
19
|
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
|
20
|
-
import type
|
|
20
|
+
import { buildSkillPromptMessage, type Skill } from "../extensibility/skills";
|
|
21
21
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
22
22
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
23
23
|
import { callTool } from "../mcp/client";
|
|
@@ -29,6 +29,7 @@ import { createAgentSession, discoverAuthStorage } from "../sdk";
|
|
|
29
29
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
30
30
|
import type { ArtifactManager } from "../session/artifacts";
|
|
31
31
|
import type { AuthStorage } from "../session/auth-storage";
|
|
32
|
+
import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
|
|
32
33
|
import { SessionManager } from "../session/session-manager";
|
|
33
34
|
import { truncateTail } from "../session/streaming-output";
|
|
34
35
|
import type { ContextFileEntry } from "../tools";
|
|
@@ -190,6 +191,8 @@ export interface ExecutorOptions {
|
|
|
190
191
|
* transition explicitly.
|
|
191
192
|
*/
|
|
192
193
|
parentTelemetry?: AgentTelemetryConfig;
|
|
194
|
+
/** Skills to autoload via sendCustomMessage before the first prompt */
|
|
195
|
+
autoloadSkills?: Skill[];
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
function parseStringifiedJson(value: unknown): unknown {
|
|
@@ -1347,6 +1350,30 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1347
1350
|
|
|
1348
1351
|
const MAX_YIELD_RETRIES = 3;
|
|
1349
1352
|
unsubscribe = session.subscribe(event => {
|
|
1353
|
+
if (event.type === "auto_retry_start") {
|
|
1354
|
+
progress.retryState = {
|
|
1355
|
+
attempt: event.attempt,
|
|
1356
|
+
maxAttempts: event.maxAttempts,
|
|
1357
|
+
delayMs: event.delayMs,
|
|
1358
|
+
errorMessage: event.errorMessage,
|
|
1359
|
+
startedAtMs: Date.now(),
|
|
1360
|
+
};
|
|
1361
|
+
progress.retryFailure = undefined;
|
|
1362
|
+
scheduleProgress(true);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (event.type === "auto_retry_end") {
|
|
1366
|
+
const attempt = progress.retryState?.attempt ?? event.attempt;
|
|
1367
|
+
progress.retryState = undefined;
|
|
1368
|
+
if (!event.success) {
|
|
1369
|
+
progress.retryFailure = {
|
|
1370
|
+
attempt,
|
|
1371
|
+
errorMessage: event.finalError ?? "Auto-retry failed",
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
scheduleProgress(true);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1350
1377
|
if (isAgentEvent(event)) {
|
|
1351
1378
|
try {
|
|
1352
1379
|
processEvent(event);
|
|
@@ -1360,6 +1387,21 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1360
1387
|
});
|
|
1361
1388
|
|
|
1362
1389
|
checkAbort();
|
|
1390
|
+
// Autoload skills via sendCustomMessage (same mechanic as /skill:<name>)
|
|
1391
|
+
if (options.autoloadSkills?.length) {
|
|
1392
|
+
for (const skill of options.autoloadSkills) {
|
|
1393
|
+
const { message } = await buildSkillPromptMessage(skill, "");
|
|
1394
|
+
await session.sendCustomMessage(
|
|
1395
|
+
{
|
|
1396
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
1397
|
+
content: message,
|
|
1398
|
+
display: false,
|
|
1399
|
+
details: { name: skill.name, path: skill.filePath },
|
|
1400
|
+
},
|
|
1401
|
+
{ triggerTurn: false },
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1363
1405
|
await awaitAbortable(session.prompt(task, { attribution: "agent" }));
|
|
1364
1406
|
await awaitAbortable(session.waitForIdle());
|
|
1365
1407
|
|
|
@@ -1367,6 +1409,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1367
1409
|
|
|
1368
1410
|
let retryCount = 0;
|
|
1369
1411
|
while (!yieldCalled && retryCount < MAX_YIELD_RETRIES && !abortSignal.aborted) {
|
|
1412
|
+
// Skip reminders when the model returned a terminal error (e.g.
|
|
1413
|
+
// rate-limit cap hit, auth failure). Re-prompting would just
|
|
1414
|
+
// hit the same wall, multiplying the failure noise without
|
|
1415
|
+
// any chance of producing a yield.
|
|
1416
|
+
const lastBeforeReminder = session.getLastAssistantMessage();
|
|
1417
|
+
if (lastBeforeReminder?.stopReason === "error") break;
|
|
1370
1418
|
try {
|
|
1371
1419
|
retryCount++;
|
|
1372
1420
|
const reminder = prompt.render(submitReminderTemplate, {
|
|
@@ -1566,6 +1614,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1566
1614
|
usage: hasUsage ? accumulatedUsage : undefined,
|
|
1567
1615
|
outputPath,
|
|
1568
1616
|
extractedToolData: progress.extractedToolData,
|
|
1617
|
+
retryFailure: progress.retryFailure,
|
|
1569
1618
|
outputMeta,
|
|
1570
1619
|
};
|
|
1571
1620
|
}
|
package/src/task/index.ts
CHANGED
|
@@ -410,6 +410,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
410
410
|
progress.contextWindow = singleResult?.contextWindow;
|
|
411
411
|
progress.cost = singleResult?.usage?.cost.total ?? 0;
|
|
412
412
|
progress.extractedToolData = singleResult?.extractedToolData;
|
|
413
|
+
progress.retryFailure = singleResult?.retryFailure;
|
|
414
|
+
progress.retryState = undefined;
|
|
413
415
|
}
|
|
414
416
|
completedJobs += 1;
|
|
415
417
|
if (singleResult && ((singleResult.aborted ?? false) || singleResult.exitCode !== 0)) {
|
|
@@ -830,6 +832,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
830
832
|
const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: uniqueIds[i] }));
|
|
831
833
|
|
|
832
834
|
const availableSkills = [...(this.session.skills ?? [])];
|
|
835
|
+
// Resolve autoload skills from agent definition against available skills
|
|
836
|
+
const resolvedAutoloadSkills =
|
|
837
|
+
agent.autoloadSkills?.length && availableSkills.length > 0
|
|
838
|
+
? agent.autoloadSkills
|
|
839
|
+
.map(name => availableSkills.find(s => s.name === name))
|
|
840
|
+
.filter((s): s is NonNullable<typeof s> => s !== undefined)
|
|
841
|
+
: [];
|
|
833
842
|
const contextFiles = this.session.contextFiles?.filter(
|
|
834
843
|
file => path.basename(file.path).toLowerCase() !== "agents.md",
|
|
835
844
|
);
|
|
@@ -894,6 +903,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
894
903
|
mcpManager: MCPManager.instance(),
|
|
895
904
|
contextFiles,
|
|
896
905
|
skills: availableSkills,
|
|
906
|
+
autoloadSkills: resolvedAutoloadSkills,
|
|
897
907
|
workspaceTree: this.session.workspaceTree,
|
|
898
908
|
promptTemplates,
|
|
899
909
|
localProtocolOptions,
|
|
@@ -948,6 +958,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
948
958
|
mcpManager: MCPManager.instance(),
|
|
949
959
|
contextFiles,
|
|
950
960
|
skills: availableSkills,
|
|
961
|
+
autoloadSkills: resolvedAutoloadSkills,
|
|
951
962
|
workspaceTree: this.session.workspaceTree,
|
|
952
963
|
promptTemplates,
|
|
953
964
|
localProtocolOptions,
|
package/src/task/render.ts
CHANGED
|
@@ -551,8 +551,15 @@ function renderAgentProgress(
|
|
|
551
551
|
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
552
552
|
let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`;
|
|
553
553
|
|
|
554
|
-
//
|
|
555
|
-
|
|
554
|
+
// Show retry-blocked badge so the parent immediately sees that a child
|
|
555
|
+
// is sleeping on a provider 429, not silently progressing. Wins over the
|
|
556
|
+
// generic running spinner because "we're waiting on a quota window" is
|
|
557
|
+
// the operationally meaningful state.
|
|
558
|
+
if (progress.retryState && progress.status === "running") {
|
|
559
|
+
statusLine += ` ${formatBadge("retrying", "warning", theme)}`;
|
|
560
|
+
} else if (progress.retryFailure && (progress.status === "failed" || progress.status === "aborted")) {
|
|
561
|
+
statusLine += ` ${formatBadge("rate-limited", "error", theme)}`;
|
|
562
|
+
} else if (progress.status === "failed" || progress.status === "aborted") {
|
|
556
563
|
const statusLabel = progress.status === "failed" ? "failed" : "aborted";
|
|
557
564
|
statusLine += ` ${formatBadge(statusLabel, iconColor, theme)}`;
|
|
558
565
|
}
|
|
@@ -598,6 +605,23 @@ function renderAgentProgress(
|
|
|
598
605
|
}
|
|
599
606
|
}
|
|
600
607
|
|
|
608
|
+
// Retry detail line: surface why the subagent is paused and roughly how
|
|
609
|
+
// long until the next attempt. Without this, the parent UI would just
|
|
610
|
+
// keep spinning while a child sleeps on a 3-hour provider rate-limit.
|
|
611
|
+
if (progress.retryState && progress.status === "running") {
|
|
612
|
+
const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - Date.now());
|
|
613
|
+
const waitLabel = remainingMs > 0 ? `in ${formatDuration(remainingMs)}` : "now";
|
|
614
|
+
const summary =
|
|
615
|
+
`retrying ${progress.retryState.attempt}/${progress.retryState.maxAttempts} ${waitLabel}: ` +
|
|
616
|
+
truncateToWidth(replaceTabs(progress.retryState.errorMessage), 60);
|
|
617
|
+
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
|
|
618
|
+
} else if (progress.retryFailure && progress.status !== "running") {
|
|
619
|
+
const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
|
|
620
|
+
progress.retryFailure.attempt === 1 ? "" : "s"
|
|
621
|
+
}: ${truncateToWidth(replaceTabs(progress.retryFailure.errorMessage), 80)}`;
|
|
622
|
+
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("error", summary)}`);
|
|
623
|
+
}
|
|
624
|
+
|
|
601
625
|
// Render extracted tool data inline (e.g., review findings)
|
|
602
626
|
if (progress.extractedToolData) {
|
|
603
627
|
// For completed tasks, check for review verdict from yield tool
|