pi-ui-extend 1.0.24 → 1.0.30
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/README.md +5 -1
- package/dist/app/app.d.ts +1 -0
- package/dist/app/app.js +12 -0
- package/dist/app/icons.d.ts +1 -0
- package/dist/app/icons.js +2 -0
- package/dist/app/input/input-controller.d.ts +2 -0
- package/dist/app/input/input-controller.js +8 -5
- package/dist/app/rendering/render-controller.js +2 -0
- package/dist/app/rendering/status-line-renderer.d.ts +4 -1
- package/dist/app/rendering/status-line-renderer.js +12 -0
- package/dist/app/screen/mouse-controller.d.ts +4 -1
- package/dist/app/screen/mouse-controller.js +9 -0
- package/dist/app/session/agent-pause-controller.d.ts +30 -0
- package/dist/app/session/agent-pause-controller.js +178 -0
- package/dist/app/session/session-event-controller.d.ts +1 -0
- package/dist/app/session/session-event-controller.js +22 -15
- package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
- package/dist/app/session/session-lifecycle-controller.js +1 -0
- package/dist/app/types.d.ts +10 -0
- package/dist/config.js +1 -1
- package/dist/default-pix-config.js +2 -2
- package/external/pi-tools-suite/README.md +1 -1
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +34 -2
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +24 -18
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +14 -3
- package/external/pi-tools-suite/src/async-subagents/core/process.ts +19 -0
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +97 -17
- package/external/pi-tools-suite/src/async-subagents/core/stop.ts +4 -2
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/SKILL.md +150 -19
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +78 -0
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +80 -0
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-flow.example.jsonc +54 -1
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/scripts/browser-qa-runner.mjs +1260 -79
- package/external/pi-tools-suite/src/dcp/index.ts +21 -0
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +92 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +21 -16
- package/external/pi-tools-suite/src/todo/index.ts +86 -46
- package/external/pi-tools-suite/src/todo/todo.ts +1 -1
- package/package.json +1 -1
package/dist/app/types.d.ts
CHANGED
|
@@ -284,6 +284,7 @@ export type StatusLineLayout = {
|
|
|
284
284
|
compactToolsWidget?: StatusCompactToolsWidgetLayout;
|
|
285
285
|
terminalBellSoundWidget?: StatusTerminalBellSoundWidgetLayout;
|
|
286
286
|
promptEnhancerWidget?: StatusPromptEnhancerWidgetLayout;
|
|
287
|
+
agentPauseWidget?: StatusAgentPauseWidgetLayout;
|
|
287
288
|
voiceWidget?: StatusVoiceWidgetLayout;
|
|
288
289
|
};
|
|
289
290
|
export type StatusQuickScrollWidgetLayout = {
|
|
@@ -318,6 +319,10 @@ export type StatusPromptEnhancerWidgetLayout = {
|
|
|
318
319
|
startColumn: number;
|
|
319
320
|
endColumn: number;
|
|
320
321
|
};
|
|
322
|
+
export type StatusAgentPauseWidgetLayout = {
|
|
323
|
+
startColumn: number;
|
|
324
|
+
endColumn: number;
|
|
325
|
+
};
|
|
321
326
|
export type StatusTerminalBellSoundTarget = {
|
|
322
327
|
row: number;
|
|
323
328
|
startColumn: number;
|
|
@@ -589,6 +594,11 @@ export type StatusPromptEnhancerTarget = {
|
|
|
589
594
|
startColumn: number;
|
|
590
595
|
endColumn: number;
|
|
591
596
|
};
|
|
597
|
+
export type StatusAgentPauseTarget = {
|
|
598
|
+
row: number;
|
|
599
|
+
startColumn: number;
|
|
600
|
+
endColumn: number;
|
|
601
|
+
};
|
|
592
602
|
export type StatusUserJumpTarget = {
|
|
593
603
|
row: number;
|
|
594
604
|
startColumn: number;
|
package/dist/config.js
CHANGED
|
@@ -55,7 +55,7 @@ const DEFAULT_OUTPUT_FILTERS = {
|
|
|
55
55
|
};
|
|
56
56
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
57
57
|
const DEFAULT_PROMPT_ENHANCER = {
|
|
58
|
-
modelRef: "
|
|
58
|
+
modelRef: "openai-codex/gpt-5.6-luna",
|
|
59
59
|
};
|
|
60
60
|
const DEFAULT_AUTOCOMPLETE = {
|
|
61
61
|
modelRef: "zai/glm-5-turbo",
|
|
@@ -52,9 +52,9 @@ export const DEFAULT_PIX_CONFIG_JSONC = String.raw `{
|
|
|
52
52
|
"antigravity/antigravity-claude-*": "error"
|
|
53
53
|
},
|
|
54
54
|
|
|
55
|
-
"promptEnhancer": { "modelRef": "
|
|
55
|
+
"promptEnhancer": { "modelRef": "openai-codex/gpt-5.6-luna" },
|
|
56
56
|
"autocomplete": { "modelRef": "zai/glm-5-turbo", "debounceMs": 350, "timeoutMs": 3000, "maxTokens": 48, "maxPromptTokens": 1200, "includeRecentMessages": 0 },
|
|
57
|
-
"sessionTitle": { "modelRef": "
|
|
57
|
+
"sessionTitle": { "modelRef": "openai-codex/gpt-5.6-luna", "fallbackModels": ["zai/glm-5-turbo"] },
|
|
58
58
|
"dictation": {
|
|
59
59
|
"language": "en",
|
|
60
60
|
"languages": {
|
|
@@ -247,7 +247,7 @@ For an oh-my-openagent-style workflow, run `/ultrawork` or `/ulw` to ask the par
|
|
|
247
247
|
|
|
248
248
|
### Private browser QA and project auth
|
|
249
249
|
|
|
250
|
-
The built-in `browser-qa` role runs on `openai-codex/gpt-5.
|
|
250
|
+
The built-in `browser-qa` role runs on `openai-codex/gpt-5.6-luna`, with
|
|
251
251
|
`antigravity/gemini-3-flash-preview` and then `zai/glm-5.3` as fallbacks. Its browser
|
|
252
252
|
workflow is an explicit private skill under `src/async-subagents/private-skills/`,
|
|
253
253
|
outside normal Pi skill discovery. The role's first-class `isolatedSkills` setting launches the child with
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Provide a cheap, fast `browser-qa` async-subagent that reproduces browser bugs
|
|
6
6
|
and proves fixes with deterministic assertions plus screenshot, video, and trace
|
|
7
|
-
evidence. The role uses `openai-codex/gpt-5.
|
|
7
|
+
evidence. The role uses `openai-codex/gpt-5.6-luna`, falling back to
|
|
8
8
|
`antigravity/gemini-3-flash-preview` and then `zai/glm-5.3`.
|
|
9
9
|
|
|
10
10
|
## Private skill isolation
|
|
@@ -62,6 +62,9 @@ evidence. The role uses `openai-codex/gpt-5.4-mini`, falling back to
|
|
|
62
62
|
trusted runner implements a bounded set of navigation, interaction,
|
|
63
63
|
assertion, screenshot, and auth-rejection actions and never gives the flow a
|
|
64
64
|
Playwright context or credential values.
|
|
65
|
+
- Target discovery is a bounded preflight, not an open-ended research task. The
|
|
66
|
+
sub-agent invokes the runner within 45 seconds or returns `BLOCKED`; it does
|
|
67
|
+
not spend the full launcher budget reading source or probing prerequisites.
|
|
65
68
|
- The launcher injects `PI_SUBAGENT_AGENT_DIR`, pre-creates a private
|
|
66
69
|
`browser-qa/flows/` workspace, and clears stale browser QA files when an agent
|
|
67
70
|
id is reused. The runner validates the directory's project/type metadata and
|
|
@@ -79,6 +82,32 @@ evidence. The role uses `openai-codex/gpt-5.4-mini`, falling back to
|
|
|
79
82
|
- Auth rejection discovered by a QA flow is reported through the
|
|
80
83
|
`authRejectedIf` action so the parent gets an update-required status.
|
|
81
84
|
|
|
85
|
+
## Reliability and shutdown contract
|
|
86
|
+
|
|
87
|
+
- The built-in `browser-qa` profile has a 120-second wall-clock budget unless
|
|
88
|
+
the caller explicitly supplies a task or spawn timeout. This bounds model
|
|
89
|
+
stalls as well as browser work.
|
|
90
|
+
- The trusted runner has its own bounded lifecycle. Browser launch, context
|
|
91
|
+
setup, auth, flow execution, evidence finalization, and browser shutdown must
|
|
92
|
+
not wait forever; a timeout reports the last started stage without exposing
|
|
93
|
+
flow contents or credentials.
|
|
94
|
+
- Trace sanitization runs in a memory-limited worker that can be terminated at
|
|
95
|
+
the cleanup deadline; synchronous archive work cannot defeat the watchdog.
|
|
96
|
+
- The launcher always writes a small sanitized `progress.jsonl` journal in the
|
|
97
|
+
agent directory. It records lifecycle/RPC event types and tool names, but not
|
|
98
|
+
prompts, tool arguments, tool results, model text, or secrets. The browser
|
|
99
|
+
runner writes similarly sanitized stage entries under its private workspace.
|
|
100
|
+
- On POSIX, newly launched agents own a process group. Settled, timed-out, and
|
|
101
|
+
explicitly stopped agents signal that group rather than only the Pi process;
|
|
102
|
+
timeout/settled shutdown escalates to `SIGKILL` after its grace period. On
|
|
103
|
+
Windows the existing recursive `taskkill /T /F` behavior remains in force.
|
|
104
|
+
- Process-tree cleanup is scoped to a launcher-created process-group marker so
|
|
105
|
+
an old or externally-created PID is never treated as an owned process group.
|
|
106
|
+
User browser sessions outside that group must not be signalled.
|
|
107
|
+
- Playwright can launch Chromium in its own POSIX process group. On runner
|
|
108
|
+
failure the runner snapshots and kills only its own descendants before it
|
|
109
|
+
exits, covering that detached browser tree without touching a user's browser.
|
|
110
|
+
|
|
82
111
|
## Acceptance criteria
|
|
83
112
|
|
|
84
113
|
1. `browser-qa` resolves to the intended model/fallback and its self-contained
|
|
@@ -96,7 +125,10 @@ evidence. The role uses `openai-codex/gpt-5.4-mini`, falling back to
|
|
|
96
125
|
deleting the run removes them while persistent auth config/state remains.
|
|
97
126
|
6. Completed test runs report clickable screenshot, video, and trace links
|
|
98
127
|
whenever those artifacts exist.
|
|
99
|
-
7.
|
|
128
|
+
7. Timeout tests identify the last browser stage, launcher progress remains
|
|
129
|
+
available when full RPC logging is disabled, and process-tree tests prove a
|
|
130
|
+
descendant is terminated without signalling unrelated processes.
|
|
131
|
+
8. Suite tests/typecheck, host checks, and suite sync pass.
|
|
100
132
|
|
|
101
133
|
## Real-browser regression test
|
|
102
134
|
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"model": "zai/glm-4.5-air",
|
|
72
72
|
// Ordered router model fallbacks tried when the primary routing model is
|
|
73
73
|
// unavailable or fails. The current parent model is always tried last.
|
|
74
|
-
"fallbackModels": ["openai-codex/gpt-5.
|
|
74
|
+
"fallbackModels": ["openai-codex/gpt-5.6-luna"],
|
|
75
75
|
"maxTaskChars": 1200,
|
|
76
76
|
"maxTokens": 512,
|
|
77
77
|
"maxRetries": 3,
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"research": { "model": "zai/glm-5-turbo", "thinking": "low" },
|
|
117
117
|
"docs": { "model": "zai/glm-4.5-air", "thinking": "low" },
|
|
118
118
|
"frontend": { "model": "antigravity/gemini-3-flash-preview", "fallbackModels": ["zai/glm-5.3"], "thinking": "medium" },
|
|
119
|
-
"browser-qa": { "model": "openai-codex/gpt-5.
|
|
119
|
+
"browser-qa": { "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "low" },
|
|
120
120
|
"tests": { "model": "zai/glm-5-turbo", "thinking": "medium" },
|
|
121
121
|
"review": { "model": "zai/glm-5.3", "thinking": "high" },
|
|
122
122
|
"implement": { "model": "zai/glm-5.3", "thinking": "high" },
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"research": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["zai/glm-5-turbo"], "thinking": "low" },
|
|
133
133
|
"docs": { "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["zai/glm-4.5-air"], "thinking": "low" },
|
|
134
134
|
"frontend": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "medium" },
|
|
135
|
-
"browser-qa": { "model": "openai-codex/gpt-5.
|
|
135
|
+
"browser-qa": { "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "low" },
|
|
136
136
|
"tests": { "model": "openai-codex/gpt-5.6-terra", "fallbackModels": ["zai/glm-5-turbo"], "thinking": "medium" },
|
|
137
137
|
"review": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
|
|
138
138
|
"implement": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
|
|
@@ -143,13 +143,13 @@
|
|
|
143
143
|
"deep": {
|
|
144
144
|
"description": "Use only enabled strong models, mixing GPT, Claude, and Gemini by role.",
|
|
145
145
|
"types": {
|
|
146
|
-
"quick": { "model": "openai-codex/gpt-5.
|
|
147
|
-
"scan": { "model": "antigravity/gemini-3-flash-preview", "fallbackModels": ["openai-codex/gpt-5.
|
|
148
|
-
"research": { "model": "antigravity/gemini-3.1-pro-preview", "fallbackModels": ["openai-codex/gpt-5.
|
|
149
|
-
"docs": { "model": "antigravity/gemini-2.5-flash", "fallbackModels": ["openai-codex/gpt-5.
|
|
150
|
-
"frontend": { "model": "antigravity/gemini-3.1-pro-preview-customtools", "fallbackModels": ["openai-codex/gpt-5.
|
|
151
|
-
"browser-qa": { "model": "openai-codex/gpt-5.
|
|
152
|
-
"tests": { "model": "antigravity/antigravity-claude-sonnet-4-6", "fallbackModels": ["openai-codex/gpt-5.
|
|
146
|
+
"quick": { "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["zai/glm-4.5-air"], "thinking": "low" },
|
|
147
|
+
"scan": { "model": "antigravity/gemini-3-flash-preview", "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-4.5-air"], "thinking": "off" },
|
|
148
|
+
"research": { "model": "antigravity/gemini-3.1-pro-preview", "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-5-turbo"], "thinking": "medium" },
|
|
149
|
+
"docs": { "model": "antigravity/gemini-2.5-flash", "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-4.5-air"], "thinking": "medium" },
|
|
150
|
+
"frontend": { "model": "antigravity/gemini-3.1-pro-preview-customtools", "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-5.3"], "thinking": "low" },
|
|
151
|
+
"browser-qa": { "model": "openai-codex/gpt-5.6-luna", "fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"], "thinking": "low" },
|
|
152
|
+
"tests": { "model": "antigravity/antigravity-claude-sonnet-4-6", "fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-5-turbo"], "thinking": "high" },
|
|
153
153
|
"review": { "model": "antigravity/antigravity-claude-sonnet-4-6", "fallbackModels": ["openai-codex/gpt-5.6-sol", "zai/glm-5.3"], "thinking": "high" },
|
|
154
154
|
"implement": { "model": "openai-codex/gpt-5.6-sol", "fallbackModels": ["zai/glm-5.3"], "thinking": "high" },
|
|
155
155
|
"deep": { "model": "antigravity/antigravity-claude-opus-4-6-thinking", "fallbackModels": ["openai-codex/gpt-5.6-sol", "zai/glm-5.3"], "thinking": "high" }
|
|
@@ -160,34 +160,38 @@
|
|
|
160
160
|
"types": {
|
|
161
161
|
"quick": {
|
|
162
162
|
"description": "Use for tiny cheap tasks: answer a simple question, inspect one known file, or verify one fact. Not for broad repo search.",
|
|
163
|
-
"model": "
|
|
163
|
+
"model": "openai-codex/gpt-5.6-luna",
|
|
164
|
+
"fallbackModels": ["zai/glm-4.5-air"],
|
|
164
165
|
"thinking": "off"
|
|
165
166
|
},
|
|
166
167
|
|
|
167
168
|
"scan": {
|
|
168
169
|
"description": "Use for finding files, symbols, text, or inventory across a repo. Return paths/facts; do not judge code quality.",
|
|
169
|
-
"model": "
|
|
170
|
+
"model": "openai-codex/gpt-5.6-luna",
|
|
171
|
+
"fallbackModels": ["zai/glm-4.5-air"],
|
|
170
172
|
"thinking": "off",
|
|
171
173
|
"tools": ["read", "grep"]
|
|
172
174
|
},
|
|
173
175
|
|
|
174
176
|
"research": {
|
|
175
177
|
"description": "Use for multi-file codebase research: read several files and explain how something works. No edits.",
|
|
176
|
-
"model": "
|
|
178
|
+
"model": "openai-codex/gpt-5.6-terra",
|
|
179
|
+
"fallbackModels": ["zai/glm-5-turbo"],
|
|
177
180
|
"thinking": "low",
|
|
178
181
|
"tools": ["read", "grep"]
|
|
179
182
|
},
|
|
180
183
|
|
|
181
184
|
"docs": {
|
|
182
185
|
"description": "Use for documentation work: README/API docs review, docs gaps, changelog, migration notes, examples.",
|
|
183
|
-
"model": "
|
|
186
|
+
"model": "openai-codex/gpt-5.6-luna",
|
|
187
|
+
"fallbackModels": ["zai/glm-4.5-air"],
|
|
184
188
|
"thinking": "low"
|
|
185
189
|
},
|
|
186
190
|
|
|
187
191
|
"frontend": {
|
|
188
192
|
"description": "Use for frontend UI/UX visual work: styling, layout, typography, animation, responsive states, component polish, accessibility. Avoid backend/business logic unless needed for UI behavior.",
|
|
189
193
|
"model": "antigravity/gemini-3-flash-preview",
|
|
190
|
-
"fallbackModels": ["openai-codex/gpt-5.
|
|
194
|
+
"fallbackModels": ["openai-codex/gpt-5.6-luna", "zai/glm-5.3"],
|
|
191
195
|
"thinking": "medium",
|
|
192
196
|
"promptAppend": [
|
|
193
197
|
"Act as a frontend UI/UX engineer for visual and product-facing work.",
|
|
@@ -199,15 +203,17 @@
|
|
|
199
203
|
|
|
200
204
|
"browser-qa": {
|
|
201
205
|
"description": "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
|
|
202
|
-
"model": "openai-codex/gpt-5.
|
|
206
|
+
"model": "openai-codex/gpt-5.6-luna",
|
|
203
207
|
"fallbackModels": ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
|
|
204
|
-
"thinking": "
|
|
208
|
+
"thinking": "low",
|
|
209
|
+
"timeoutMs": 120000,
|
|
205
210
|
"tools": ["read", "grep", "bash"]
|
|
206
211
|
},
|
|
207
212
|
|
|
208
213
|
"tests": {
|
|
209
214
|
"description": "Use for tests: locate coverage, find gaps, run/check targeted test commands, diagnose failing tests.",
|
|
210
|
-
"model": "
|
|
215
|
+
"model": "openai-codex/gpt-5.6-terra",
|
|
216
|
+
"fallbackModels": ["zai/glm-5-turbo"],
|
|
211
217
|
"thinking": "medium",
|
|
212
218
|
"tools": ["read", "grep", "bash"]
|
|
213
219
|
},
|
|
@@ -167,7 +167,7 @@ export const DEFAULT_MAX_CONCURRENT = 5;
|
|
|
167
167
|
export const DEFAULT_ROUTING_CONFIG: ResolvedSubagentRoutingConfig = {
|
|
168
168
|
enabled: true,
|
|
169
169
|
model: "zai/glm-4.5-air",
|
|
170
|
-
fallbackModels: ["openai-codex/gpt-5.
|
|
170
|
+
fallbackModels: ["openai-codex/gpt-5.6-luna"],
|
|
171
171
|
maxTaskChars: 1200,
|
|
172
172
|
maxTokens: 512,
|
|
173
173
|
maxRetries: 3,
|
|
@@ -181,18 +181,26 @@ const BUILTIN_CONFIG: SubagentConfig = {
|
|
|
181
181
|
types: {
|
|
182
182
|
quick: {
|
|
183
183
|
description: "Use for tiny cheap tasks: answer a simple question, inspect one known file, or verify one fact. Not for broad repo search.",
|
|
184
|
+
model: "openai-codex/gpt-5.6-luna",
|
|
185
|
+
fallbackModels: ["zai/glm-4.5-air"],
|
|
184
186
|
thinking: "off",
|
|
185
187
|
},
|
|
186
188
|
scan: {
|
|
187
189
|
description: "Use for finding files, symbols, text, or inventory across a repo. Return paths/facts; do not judge code quality.",
|
|
190
|
+
model: "openai-codex/gpt-5.6-luna",
|
|
191
|
+
fallbackModels: ["zai/glm-4.5-air"],
|
|
188
192
|
thinking: "off",
|
|
189
193
|
},
|
|
190
194
|
research: {
|
|
191
195
|
description: "Use for multi-file codebase research: read several files and explain how something works. No edits.",
|
|
196
|
+
model: "openai-codex/gpt-5.6-terra",
|
|
197
|
+
fallbackModels: ["zai/glm-5-turbo"],
|
|
192
198
|
thinking: "low",
|
|
193
199
|
},
|
|
194
200
|
docs: {
|
|
195
201
|
description: "Use for documentation work: README/API docs review, docs gaps, changelog, migration notes, examples.",
|
|
202
|
+
model: "openai-codex/gpt-5.6-luna",
|
|
203
|
+
fallbackModels: ["zai/glm-4.5-air"],
|
|
196
204
|
thinking: "low",
|
|
197
205
|
},
|
|
198
206
|
frontend: {
|
|
@@ -207,9 +215,10 @@ const BUILTIN_CONFIG: SubagentConfig = {
|
|
|
207
215
|
},
|
|
208
216
|
"browser-qa": {
|
|
209
217
|
description: "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
|
|
210
|
-
model: "openai-codex/gpt-5.
|
|
218
|
+
model: "openai-codex/gpt-5.6-luna",
|
|
211
219
|
fallbackModels: ["antigravity/gemini-3-flash-preview", "zai/glm-5.3"],
|
|
212
|
-
thinking: "
|
|
220
|
+
thinking: "low",
|
|
221
|
+
timeoutMs: 120_000,
|
|
213
222
|
tools: ["read", "grep", "bash"],
|
|
214
223
|
isolatedSkills: [getBrowserQaSkillPath()],
|
|
215
224
|
},
|
|
@@ -219,6 +228,8 @@ const BUILTIN_CONFIG: SubagentConfig = {
|
|
|
219
228
|
},
|
|
220
229
|
tests: {
|
|
221
230
|
description: "Use for tests: locate coverage, find gaps, run/check targeted test commands, diagnose failing tests.",
|
|
231
|
+
model: "openai-codex/gpt-5.6-terra",
|
|
232
|
+
fallbackModels: ["zai/glm-5-turbo"],
|
|
222
233
|
thinking: "medium",
|
|
223
234
|
},
|
|
224
235
|
review: {
|
|
@@ -13,3 +13,22 @@ export function terminateProcess(pid: number, signal: ProcessSignal): void {
|
|
|
13
13
|
}
|
|
14
14
|
process.kill(pid, signal);
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
/** Signal a launcher-owned process tree. POSIX callers must create the child as
|
|
18
|
+
* a process-group leader before using this helper. */
|
|
19
|
+
export function terminateProcessTree(pid: number, signal: ProcessSignal): void {
|
|
20
|
+
if (process.platform === "win32") {
|
|
21
|
+
terminateProcess(pid, signal);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
process.kill(-pid, signal);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const code = typeof error === "object" && error && "code" in error
|
|
29
|
+
? String((error as { code?: unknown }).code)
|
|
30
|
+
: undefined;
|
|
31
|
+
if (code !== "ESRCH") throw error;
|
|
32
|
+
// The owned group is already gone. Never fall back to a possibly reused PID.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -6,7 +6,7 @@ import { selectSuitableToolsForModel } from "../../lib/tool-args.js";
|
|
|
6
6
|
import { validateBasename } from "./paths.js";
|
|
7
7
|
import { getPiInvocation } from "./pi-invocation.js";
|
|
8
8
|
import { writePromptFile } from "./prompt.js";
|
|
9
|
-
import {
|
|
9
|
+
import { terminateProcessTree } from "./process.js";
|
|
10
10
|
import { getAgentSessionDir, SUBAGENT_PARENT_SESSION_FILE, SUBAGENT_RETURN_SESSION_FILE, SUBAGENT_SESSION_FILE, writeParentSessionLink, writeSessionFileLink } from "./sessions.js";
|
|
11
11
|
import { getAgentState } from "./state.js";
|
|
12
12
|
import { writeStructuredResult } from "./structured-result.js";
|
|
@@ -31,6 +31,7 @@ const AGENT_TIMEOUT_KILL_GRACE_MS = 5_000;
|
|
|
31
31
|
const AGENT_SETTLED_TERMINATE_GRACE_MS = 50;
|
|
32
32
|
const AGENT_SETTLED_COMPLETION_FALLBACK_MS = 1_000;
|
|
33
33
|
const EXIT_STDIO_FLUSH_GRACE_MS = 10;
|
|
34
|
+
const PROGRESS_LOG_MAX_BYTES = 1024 * 1024;
|
|
34
35
|
|
|
35
36
|
export function shouldPersistSubagentSessions(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
36
37
|
return isTruthyEnv(env.ASYNC_SUBAGENTS_ENABLE_SESSIONS);
|
|
@@ -136,11 +137,16 @@ export function spawnAgent(
|
|
|
136
137
|
const invocation = getPiInvocation(piArgs);
|
|
137
138
|
const stderrStream = createDeferredFileWriter(stderrFile, logLimits.stderrMaxBytes, "stderr.log");
|
|
138
139
|
const transcriptStream = createBoundedFileWriter(transcriptFile, logLimits.eventsMaxBytes, "events.jsonl");
|
|
140
|
+
const progressStream = createBoundedFileWriter(path.join(agentDir, "progress.jsonl"), PROGRESS_LOG_MAX_BYTES, "progress.jsonl");
|
|
141
|
+
const writeProgress = (stage: string, details: Record<string, unknown> = {}) => {
|
|
142
|
+
progressStream.write(serializeJsonLine({ at: isoNow(), stage, ...details }));
|
|
143
|
+
};
|
|
139
144
|
|
|
140
145
|
const proc = spawn(invocation.command, invocation.args, {
|
|
141
146
|
cwd,
|
|
142
147
|
env: subagentEnvironment(process.env, task.subagentType === "browser-qa" ? agentDir : undefined),
|
|
143
148
|
stdio: ["pipe", "pipe", "pipe"],
|
|
149
|
+
detached: process.platform !== "win32",
|
|
144
150
|
});
|
|
145
151
|
proc.stdin.on("error", (error: NodeJS.ErrnoException) => {
|
|
146
152
|
if (error.code === "EPIPE") return;
|
|
@@ -156,18 +162,33 @@ export function spawnAgent(
|
|
|
156
162
|
let timeoutKillTimer: NodeJS.Timeout | undefined;
|
|
157
163
|
let agentSettledKillTimer: NodeJS.Timeout | undefined;
|
|
158
164
|
let agentSettledCompletionFallbackTimer: NodeJS.Timeout | undefined;
|
|
165
|
+
let processExitTreeKillTimer: NodeJS.Timeout | undefined;
|
|
159
166
|
let exitFinalizationTimer: NodeJS.Timeout | undefined;
|
|
167
|
+
let processTermination: { code: number | null; signal: NodeJS.Signals | null } | undefined;
|
|
168
|
+
let stdoutEnded = false;
|
|
160
169
|
const suppressedRpcEventCounts = new Map<string, number>();
|
|
170
|
+
const scheduleProcessTreeKill = (reason: string) => {
|
|
171
|
+
if (processExitTreeKillTimer) return;
|
|
172
|
+
processExitTreeKillTimer = setTimeout(() => {
|
|
173
|
+
try {
|
|
174
|
+
writeProgress("shutdown_signal", { reason, signal: "SIGKILL" });
|
|
175
|
+
terminateChildProcessTree(proc, "SIGKILL");
|
|
176
|
+
} catch {
|
|
177
|
+
/* process group may already be gone */
|
|
178
|
+
}
|
|
179
|
+
}, AGENT_SETTLED_COMPLETION_FALLBACK_MS);
|
|
180
|
+
processExitTreeKillTimer.unref?.();
|
|
181
|
+
};
|
|
161
182
|
|
|
162
183
|
const notifyComplete = (exitCode: number) => {
|
|
163
184
|
if (completionNotified) return;
|
|
164
185
|
if (exitCode !== 0) shouldKeepStderr = true;
|
|
165
186
|
completionNotified = true;
|
|
166
187
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
167
|
-
if (timeoutKillTimer) clearTimeout(timeoutKillTimer);
|
|
168
188
|
if (agentSettledKillTimer) clearTimeout(agentSettledKillTimer);
|
|
169
|
-
if (agentSettledCompletionFallbackTimer) clearTimeout(agentSettledCompletionFallbackTimer);
|
|
170
189
|
if (exitFinalizationTimer) clearTimeout(exitFinalizationTimer);
|
|
190
|
+
writeProgress("completed", { exitCode });
|
|
191
|
+
progressStream.end();
|
|
171
192
|
if (!fs.existsSync(agentDir)) {
|
|
172
193
|
onComplete?.({
|
|
173
194
|
runDir,
|
|
@@ -207,6 +228,7 @@ export function spawnAgent(
|
|
|
207
228
|
|
|
208
229
|
const finalizeCompletion = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
209
230
|
if (completionNotified) return;
|
|
231
|
+
if (!timeoutKillTimer && !agentSettledCompletionFallbackTimer) scheduleProcessTreeKill("process_exit");
|
|
210
232
|
writeSuppressedRpcEventSummary(transcriptStream, suppressedRpcEventCounts);
|
|
211
233
|
const exitCode = resolveAgentExitCode({
|
|
212
234
|
timedOut,
|
|
@@ -233,19 +255,21 @@ export function spawnAgent(
|
|
|
233
255
|
agentSettledKillTimer = setTimeout(() => {
|
|
234
256
|
agentSettledKillTimer = undefined;
|
|
235
257
|
try {
|
|
236
|
-
|
|
258
|
+
writeProgress("shutdown_signal", { reason: "agent_settled", signal: "SIGTERM" });
|
|
259
|
+
terminateChildProcessTree(proc, "SIGTERM");
|
|
237
260
|
} catch {
|
|
238
261
|
/* process may have exited before the graceful termination timer fired */
|
|
239
262
|
}
|
|
240
263
|
}, AGENT_SETTLED_TERMINATE_GRACE_MS);
|
|
241
264
|
agentSettledKillTimer.unref?.();
|
|
242
265
|
agentSettledCompletionFallbackTimer = setTimeout(() => {
|
|
243
|
-
if (completionNotified) return;
|
|
244
266
|
try {
|
|
245
|
-
|
|
267
|
+
writeProgress("shutdown_signal", { reason: "agent_settled", signal: "SIGKILL" });
|
|
268
|
+
terminateChildProcessTree(proc, "SIGKILL");
|
|
246
269
|
} catch {
|
|
247
270
|
/* process may already be gone */
|
|
248
271
|
}
|
|
272
|
+
if (completionNotified) return;
|
|
249
273
|
proc.stdin.destroy();
|
|
250
274
|
proc.stdout?.destroy();
|
|
251
275
|
proc.stderr?.destroy();
|
|
@@ -261,6 +285,7 @@ export function spawnAgent(
|
|
|
261
285
|
if (completionNotified) return;
|
|
262
286
|
timedOut = true;
|
|
263
287
|
const timeoutMessage = `Sub-agent timed out after ${Math.round(timeoutMs / 1000)} seconds.`;
|
|
288
|
+
writeProgress("timeout", { timeoutMs });
|
|
264
289
|
if (fs.existsSync(agentDir)) {
|
|
265
290
|
fs.writeFileSync(path.join(agentDir, "timeout_ms"), String(timeoutMs), "utf-8");
|
|
266
291
|
fs.writeFileSync(path.join(agentDir, "timed_out_at"), isoNow(), "utf-8");
|
|
@@ -269,14 +294,15 @@ export function spawnAgent(
|
|
|
269
294
|
stderrStream.write(`${timeoutMessage}\n`);
|
|
270
295
|
}
|
|
271
296
|
try {
|
|
272
|
-
|
|
297
|
+
writeProgress("shutdown_signal", { reason: "timeout", signal: "SIGTERM" });
|
|
298
|
+
terminateChildProcessTree(proc, "SIGTERM");
|
|
273
299
|
} catch {
|
|
274
300
|
/* process may have exited between the timer and signal */
|
|
275
301
|
}
|
|
276
302
|
timeoutKillTimer = setTimeout(() => {
|
|
277
|
-
if (completionNotified) return;
|
|
278
303
|
try {
|
|
279
|
-
|
|
304
|
+
writeProgress("shutdown_signal", { reason: "timeout", signal: "SIGKILL" });
|
|
305
|
+
terminateChildProcessTree(proc, "SIGKILL");
|
|
280
306
|
} catch {
|
|
281
307
|
/* process may have exited after SIGTERM */
|
|
282
308
|
}
|
|
@@ -297,6 +323,8 @@ export function spawnAgent(
|
|
|
297
323
|
const event = JSON.parse(line) as RpcEventRecord;
|
|
298
324
|
const storedEvent = compactRpcEventForTranscript(event, Buffer.byteLength(line, "utf8"));
|
|
299
325
|
if (storedEvent) transcriptStream.write(serializeJsonLine(storedEvent));
|
|
326
|
+
const progressEvent = compactRpcEventForProgress(event);
|
|
327
|
+
if (progressEvent) writeProgress("rpc_event", progressEvent);
|
|
300
328
|
onRpcEvent?.(event);
|
|
301
329
|
const sessionFile = extractSessionFileFromEvent(event);
|
|
302
330
|
if (sessionFile) writeSessionFileLink(agentDir, sessionFile);
|
|
@@ -307,8 +335,13 @@ export function spawnAgent(
|
|
|
307
335
|
if (event.type === "response" && event.command === "prompt" && event.success === false) {
|
|
308
336
|
const errorText = typeof event.error === "string" ? event.error : "RPC prompt failed";
|
|
309
337
|
fs.writeFileSync(path.join(agentDir, "result.md"), errorText, "utf-8");
|
|
338
|
+
try {
|
|
339
|
+
terminateChildProcessTree(proc, "SIGTERM");
|
|
340
|
+
} catch {
|
|
341
|
+
/* process may have exited immediately after emitting the failure */
|
|
342
|
+
}
|
|
343
|
+
scheduleProcessTreeKill("prompt_failed");
|
|
310
344
|
notifyComplete(1);
|
|
311
|
-
terminateChildProcess(proc, "SIGTERM");
|
|
312
345
|
return;
|
|
313
346
|
}
|
|
314
347
|
if (event.type === "agent_end") {
|
|
@@ -359,10 +392,29 @@ export function spawnAgent(
|
|
|
359
392
|
},
|
|
360
393
|
});
|
|
361
394
|
|
|
362
|
-
|
|
363
|
-
|
|
395
|
+
// Bun on Windows may emit the ChildProcess `close` event before the stdout
|
|
396
|
+
// Readable has delivered its final buffered `data`/`end` events. Gate process
|
|
397
|
+
// finalization on stdout itself so a trailing RPC failure cannot be mistaken
|
|
398
|
+
// for a clean exit.
|
|
399
|
+
const finalizeAfterStdout = () => {
|
|
400
|
+
if (!processTermination || !stdoutEnded || completionNotified || exitFinalizationTimer) return;
|
|
401
|
+
const { code, signal } = processTermination;
|
|
402
|
+
exitFinalizationTimer = setTimeout(
|
|
403
|
+
() => finalizeCompletion(code, signal),
|
|
404
|
+
EXIT_STDIO_FLUSH_GRACE_MS,
|
|
405
|
+
);
|
|
364
406
|
exitFinalizationTimer.unref?.();
|
|
407
|
+
};
|
|
408
|
+
const recordProcessTermination = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
409
|
+
processTermination ??= { code, signal };
|
|
410
|
+
finalizeAfterStdout();
|
|
411
|
+
};
|
|
412
|
+
proc.stdout.once("end", () => {
|
|
413
|
+
stdoutEnded = true;
|
|
414
|
+
finalizeAfterStdout();
|
|
365
415
|
});
|
|
416
|
+
proc.once("exit", recordProcessTermination);
|
|
417
|
+
proc.once("close", recordProcessTermination);
|
|
366
418
|
|
|
367
419
|
proc.once("error", (error) => {
|
|
368
420
|
const message = String(error);
|
|
@@ -376,6 +428,11 @@ export function spawnAgent(
|
|
|
376
428
|
notifyComplete(1);
|
|
377
429
|
});
|
|
378
430
|
|
|
431
|
+
const pid = proc.pid!;
|
|
432
|
+
fs.writeFileSync(path.join(agentDir, "pid"), String(pid), "utf-8");
|
|
433
|
+
if (process.platform !== "win32") fs.writeFileSync(path.join(agentDir, "process_group"), String(pid), "utf-8");
|
|
434
|
+
writeProgress("spawned", { pid });
|
|
435
|
+
|
|
379
436
|
proc.stdin.write([
|
|
380
437
|
serializeJsonLine({
|
|
381
438
|
id: "sub_get_state",
|
|
@@ -388,6 +445,7 @@ export function spawnAgent(
|
|
|
388
445
|
...(promptImages ? { images: promptImages } : {}),
|
|
389
446
|
}),
|
|
390
447
|
].join(""));
|
|
448
|
+
writeProgress("prompt_sent");
|
|
391
449
|
// Keep stdin open while the RPC prompt is running. pi RPC mode treats stdin
|
|
392
450
|
// EOF as a shutdown request, while the prompt command itself is handled
|
|
393
451
|
// asynchronously after preflight. Closing stdin here can therefore terminate
|
|
@@ -395,9 +453,6 @@ export function spawnAgent(
|
|
|
395
453
|
// producing exit 0 with no result.md. The child is terminated explicitly after agent_settled,
|
|
396
454
|
// timeout, stop, or process error.
|
|
397
455
|
|
|
398
|
-
const pid = proc.pid!;
|
|
399
|
-
fs.writeFileSync(path.join(agentDir, "pid"), String(pid), "utf-8");
|
|
400
|
-
|
|
401
456
|
return { pid, agentDir, process: proc };
|
|
402
457
|
}
|
|
403
458
|
|
|
@@ -423,9 +478,9 @@ function getAntigravityAuthExtensionPath(): string {
|
|
|
423
478
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "antigravity-auth", "index.ts");
|
|
424
479
|
}
|
|
425
480
|
|
|
426
|
-
function
|
|
481
|
+
function terminateChildProcessTree(proc: ChildProcess, signal: NodeJS.Signals): void {
|
|
427
482
|
if (proc.pid) {
|
|
428
|
-
|
|
483
|
+
terminateProcessTree(proc.pid, signal as "SIGTERM" | "SIGINT" | "SIGKILL");
|
|
429
484
|
return;
|
|
430
485
|
}
|
|
431
486
|
proc.kill(signal);
|
|
@@ -603,6 +658,31 @@ function compactRpcEventForTranscript(event: RpcEventRecord, originalBytes: numb
|
|
|
603
658
|
return { type: event.type, bytes: originalBytes };
|
|
604
659
|
}
|
|
605
660
|
|
|
661
|
+
function compactRpcEventForProgress(event: RpcEventRecord): Record<string, unknown> | undefined {
|
|
662
|
+
if (event.type === "message_update" || event.type === "tool_execution_update") return undefined;
|
|
663
|
+
if (event.type === "response") {
|
|
664
|
+
return stripUndefined({
|
|
665
|
+
type: event.type,
|
|
666
|
+
command: typeof event.command === "string" ? event.command : undefined,
|
|
667
|
+
success: typeof event.success === "boolean" ? event.success : undefined,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
671
|
+
return stripUndefined({
|
|
672
|
+
type: event.type,
|
|
673
|
+
toolName: typeof event.toolName === "string" ? event.toolName : undefined,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
if (event.type === "message_end") {
|
|
677
|
+
return stripUndefined({
|
|
678
|
+
type: event.type,
|
|
679
|
+
role: isRecord(event.message) && typeof event.message.role === "string" ? event.message.role : undefined,
|
|
680
|
+
stopReason: isRecord(event.message) && typeof event.message.stopReason === "string" ? event.message.stopReason : undefined,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return { type: event.type };
|
|
684
|
+
}
|
|
685
|
+
|
|
606
686
|
function suppressedRpcEventType(line: string): string | undefined {
|
|
607
687
|
if (line.includes('"type":"message_update"') || line.includes('"type": "message_update"')) return "message_update";
|
|
608
688
|
if (line.includes('"type":"tool_execution_update"') || line.includes('"type": "tool_execution_update"')) return "tool_execution_update";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { getAgentState, getRunState } from "./state.js";
|
|
4
|
-
import { terminateProcess } from "./process.js";
|
|
4
|
+
import { terminateProcess, terminateProcessTree } from "./process.js";
|
|
5
5
|
import { writeStructuredResult } from "./structured-result.js";
|
|
6
6
|
import type { AgentState } from "./types.js";
|
|
7
7
|
import { isoNow } from "./utils.js";
|
|
@@ -70,7 +70,9 @@ function stopAgent(runDir: string, agent: AgentState, signal: StopSignal): StopA
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
try {
|
|
73
|
-
|
|
73
|
+
const ownsProcessGroup = fs.existsSync(path.join(runDir, agent.id, "process_group"));
|
|
74
|
+
if (ownsProcessGroup) terminateProcessTree(agent.pid, signal);
|
|
75
|
+
else terminateProcess(agent.pid, signal);
|
|
74
76
|
markStopped(runDir, agent.id, signal);
|
|
75
77
|
return {
|
|
76
78
|
...result,
|