@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.9
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 +51 -1
- package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-8yy8h3bc.md} +51 -1
- package/dist/cli.js +4443 -4444
- package/dist/types/async/job-manager.d.ts +12 -0
- package/dist/types/discovery/agents.d.ts +11 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
- package/dist/types/hindsight/state.d.ts +0 -1
- package/dist/types/lsp/utils.d.ts +8 -0
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/registry/agent-lifecycle.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +5 -3
- package/dist/types/session/turn-recovery.d.ts +6 -2
- package/dist/types/task/executor.d.ts +4 -0
- package/dist/types/tools/browser/launch.d.ts +2 -0
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/shell-tokenize.d.ts +21 -3
- package/dist/types/utils/changelog.d.ts +3 -2
- package/dist/types/utils/late-cleanup.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +2 -0
- package/package.json +13 -13
- package/scripts/omp +11 -2
- package/src/async/job-manager.ts +26 -0
- package/src/cli/stats-cli.ts +3 -3
- package/src/cli/update-cli.ts +2 -56
- package/src/config/config-file.ts +2 -2
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-discovery.ts +36 -2
- package/src/config/model-registry.ts +4 -1
- package/src/discovery/agents.ts +71 -3
- package/src/discovery/claude.ts +8 -5
- package/src/discovery/cursor.ts +8 -5
- package/src/discovery/gemini.ts +11 -8
- package/src/discovery/vscode.ts +1 -0
- package/src/discovery/windsurf.ts +4 -2
- package/src/edit/hashline/filesystem.ts +7 -7
- package/src/eval/agent-bridge.ts +1 -3
- package/src/eval/jl/prelude.jl +4 -4
- package/src/eval/js/shared/prelude.txt +2 -2
- package/src/eval/py/prelude.py +0 -3
- package/src/eval/rb/prelude.rb +1 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
- package/src/hindsight/state.ts +2 -22
- package/src/internal-urls/memory-protocol.ts +29 -0
- package/src/lsp/client.ts +2 -2
- package/src/lsp/utils.ts +29 -0
- package/src/modes/acp/acp-agent.ts +2 -1
- package/src/modes/components/agent-hub.ts +36 -20
- package/src/modes/components/agent-transcript-viewer.ts +1 -3
- package/src/modes/components/bordered-loader.ts +1 -1
- package/src/modes/components/custom-editor.ts +2 -2
- package/src/modes/components/hook-input.ts +1 -1
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/session-selector.ts +2 -2
- package/src/modes/controllers/event-controller.ts +159 -2
- package/src/modes/controllers/extension-ui-controller.ts +2 -1
- package/src/modes/print-mode.ts +29 -12
- package/src/modes/session-observer-registry.ts +5 -0
- package/src/registry/agent-lifecycle.ts +21 -3
- package/src/sdk.ts +11 -5
- package/src/session/agent-session-types.ts +8 -0
- package/src/session/agent-session.ts +156 -186
- package/src/session/session-paths.ts +68 -121
- package/src/session/settings-stream-fn.ts +7 -3
- package/src/session/turn-recovery.ts +234 -16
- package/src/session/unexpected-stop-classifier.ts +12 -3
- package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +105 -29
- package/src/task/isolation-runner.ts +19 -1
- package/src/task/structured-subagent.ts +17 -2
- package/src/tiny/worker.ts +1 -1
- package/src/tools/bash-interceptor.ts +8 -4
- package/src/tools/browser/launch.ts +37 -9
- package/src/tools/glob.ts +4 -2
- package/src/tools/path-utils.ts +13 -3
- package/src/tools/read.ts +6 -0
- package/src/tools/shell-tokenize.ts +38 -9
- package/src/utils/changelog.ts +5 -4
- package/src/utils/late-cleanup.ts +17 -0
- package/src/vibe/runtime.ts +197 -53
- package/src/web/scrapers/hackage.ts +1 -12
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/ecosia.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/firecrawl.ts +1 -1
- package/src/web/search/providers/mojeek.ts +1 -1
- package/src/web/search/providers/perplexity.ts +1 -1
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/startpage.ts +1 -1
|
@@ -83,25 +83,47 @@ export function tokenizeShellSegments(command: string): string[][] {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
86
|
+
* A flat shell command segment with the context needed to decide interception.
|
|
87
|
+
*
|
|
88
|
+
* @see extractFlatShellCommandSegments
|
|
89
|
+
*/
|
|
90
|
+
export interface FlatShellCommandSegment {
|
|
91
|
+
/** Original segment text with quoting and escaping preserved. */
|
|
92
|
+
text: string;
|
|
93
|
+
/**
|
|
94
|
+
* True when this segment consumes the previous stage's stdout via an
|
|
95
|
+
* unquoted `|` or `|&`. Blank and comment-only continuation lines preserve
|
|
96
|
+
* the pending pipe state. Such a stage reads piped stdin, so path-based
|
|
97
|
+
* dedicated tools (read/grep/glob) cannot replace it. `||`, `;`, `&`, and
|
|
98
|
+
* `&&` start an independent command and leave this false.
|
|
99
|
+
*/
|
|
100
|
+
pipedStdin: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns the flat shell command segments with the original text of each. Unlike
|
|
87
105
|
* `tokenizeShellSegments`, this preserves quoting and escaping so the results
|
|
88
|
-
* are safe to match against user-configured regular expressions
|
|
106
|
+
* are safe to match against user-configured regular expressions, and flags
|
|
107
|
+
* segments that receive piped stdin.
|
|
89
108
|
*
|
|
90
109
|
* The extractor deliberately declines to split syntax whose execution context
|
|
91
110
|
* cannot be determined with this small scanner (heredocs, command substitution,
|
|
92
111
|
* backticks, grouping, and malformed quoting). Callers must still check the
|
|
93
112
|
* complete input in that case.
|
|
94
113
|
*/
|
|
95
|
-
export function extractFlatShellCommandSegments(command: string):
|
|
96
|
-
const segments:
|
|
114
|
+
export function extractFlatShellCommandSegments(command: string): FlatShellCommandSegment[] {
|
|
115
|
+
const segments: FlatShellCommandSegment[] = [];
|
|
97
116
|
let segmentStart = 0;
|
|
98
117
|
let inSingle = false;
|
|
99
118
|
let inDouble = false;
|
|
100
119
|
let atWordStart = true;
|
|
120
|
+
let currentPiped = false;
|
|
101
121
|
|
|
102
|
-
const pushSegment = (end: number) => {
|
|
122
|
+
const pushSegment = (end: number): boolean => {
|
|
103
123
|
const segment = command.slice(segmentStart, end).trim();
|
|
104
|
-
if (segment.length
|
|
124
|
+
if (segment.length === 0) return false;
|
|
125
|
+
segments.push({ text: segment, pipedStdin: currentPiped });
|
|
126
|
+
return true;
|
|
105
127
|
};
|
|
106
128
|
|
|
107
129
|
for (let i = 0; i < command.length; i++) {
|
|
@@ -154,12 +176,14 @@ export function extractFlatShellCommandSegments(command: string): string[] {
|
|
|
154
176
|
return [];
|
|
155
177
|
}
|
|
156
178
|
if (ch === "#" && atWordStart) {
|
|
157
|
-
pushSegment(i);
|
|
179
|
+
const pushed = pushSegment(i);
|
|
158
180
|
const newline = command.indexOf("\n", i + 1);
|
|
159
181
|
if (newline === -1) return segments;
|
|
160
182
|
i = newline;
|
|
161
183
|
segmentStart = newline + 1;
|
|
162
184
|
atWordStart = true;
|
|
185
|
+
// Preserve a pending pipe through a comment-only continuation.
|
|
186
|
+
if (pushed) currentPiped = false;
|
|
163
187
|
continue;
|
|
164
188
|
}
|
|
165
189
|
const isRedirectionOperatorCharacter =
|
|
@@ -169,8 +193,13 @@ export function extractFlatShellCommandSegments(command: string): string[] {
|
|
|
169
193
|
? command[i - 1] === ">" || command[i - 1] === "<" || command[i + 1] === ">"
|
|
170
194
|
: false;
|
|
171
195
|
if ((ch === "\n" || ch === ";" || ch === "|" || ch === "&") && !isRedirectionOperatorCharacter) {
|
|
172
|
-
pushSegment(i);
|
|
173
|
-
|
|
196
|
+
const pushed = pushSegment(i);
|
|
197
|
+
const doubled = (ch === "|" || ch === "&") && command[i + 1] === ch;
|
|
198
|
+
const pipeStderr = ch === "|" && command[i + 1] === "&";
|
|
199
|
+
if (doubled || pipeStderr) i++;
|
|
200
|
+
// `|` and `|&` pipe into the next segment. Blank continuation
|
|
201
|
+
// lines preserve that pending state; all other operators reset it.
|
|
202
|
+
if (pushed || ch !== "\n") currentPiped = ch === "|" && !doubled;
|
|
174
203
|
segmentStart = i + 1;
|
|
175
204
|
atWordStart = true;
|
|
176
205
|
continue;
|
package/src/utils/changelog.ts
CHANGED
|
@@ -198,9 +198,10 @@ function parseChangelogContent(content: string): ChangelogEntry[] {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
/**
|
|
201
|
-
* Compare
|
|
201
|
+
* Compare changelog entries by their parsed version parts.
|
|
202
|
+
* Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
|
|
202
203
|
*/
|
|
203
|
-
export function
|
|
204
|
+
export function compareChangelogEntries(v1: ChangelogEntry, v2: ChangelogEntry): number {
|
|
204
205
|
if (v1.major !== v2.major) return v1.major - v2.major;
|
|
205
206
|
if (v1.minor !== v2.minor) return v1.minor - v2.minor;
|
|
206
207
|
return v1.patch - v2.patch;
|
|
@@ -232,7 +233,7 @@ export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): C
|
|
|
232
233
|
return [];
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
return entries.filter(entry =>
|
|
236
|
+
return entries.filter(entry => compareChangelogEntries(entry, parsedLastVersion) > 0);
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
/**
|
|
@@ -328,7 +329,7 @@ export async function resolveStartupChangelogForDisplay(options: {
|
|
|
328
329
|
}
|
|
329
330
|
if (options.mode === "hidden") {
|
|
330
331
|
const currentVersion = parseChangelogVersion(options.currentVersion);
|
|
331
|
-
if (currentVersion &&
|
|
332
|
+
if (currentVersion && compareChangelogEntries(currentVersion, parsedLastVersion) > 0) {
|
|
332
333
|
await writeLastChangelogVersion(options.currentVersion, options.agentDir);
|
|
333
334
|
}
|
|
334
335
|
return undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
3
|
+
const pendingCleanups = new Set<Promise<void>>();
|
|
4
|
+
|
|
5
|
+
/** Keep timed-out cleanup reachable until its resources really settle. */
|
|
6
|
+
export function trackLateCleanup(work: Promise<void>, context: Record<string, unknown>): void {
|
|
7
|
+
let tracked: Promise<void>;
|
|
8
|
+
tracked = work
|
|
9
|
+
.catch(error => {
|
|
10
|
+
logger.warn("Deferred cleanup failed", {
|
|
11
|
+
...context,
|
|
12
|
+
error: error instanceof Error ? error.message : String(error),
|
|
13
|
+
});
|
|
14
|
+
})
|
|
15
|
+
.finally(() => pendingCleanups.delete(tracked));
|
|
16
|
+
pendingCleanups.add(tracked);
|
|
17
|
+
}
|
package/src/vibe/runtime.ts
CHANGED
|
@@ -70,8 +70,8 @@ const TRACE_LINE_MAX = 120;
|
|
|
70
70
|
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
71
71
|
/** Response text cap inside a delivered turn result; full output stays at agent://<id>. */
|
|
72
72
|
const RESPONSE_PREVIEW_MAX = 6000;
|
|
73
|
-
/** Grace period for
|
|
74
|
-
const
|
|
73
|
+
/** Grace period for Vibe cancellation/release cleanup before teardown detaches (ms). */
|
|
74
|
+
const VIBE_TEARDOWN_GRACE_MS = 5_000;
|
|
75
75
|
|
|
76
76
|
const VIBE_LIFECYCLE_CUSTOM_TYPE = "vibe-session-lifecycle";
|
|
77
77
|
const VIBE_LIFECYCLE_VERSION = 1;
|
|
@@ -252,6 +252,48 @@ export interface VibeWaitOutcome {
|
|
|
252
252
|
timedOut: boolean;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
type VibeTeardownStatus = "pending" | "settled" | "failed";
|
|
256
|
+
|
|
257
|
+
interface TrackedVibeTeardown {
|
|
258
|
+
promise: Promise<void>;
|
|
259
|
+
status: () => VibeTeardownStatus;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Observe cleanup without propagating a detached late rejection. */
|
|
263
|
+
function trackVibeTeardown(promise: Promise<unknown>, onError: (error: unknown) => void): TrackedVibeTeardown {
|
|
264
|
+
let status: VibeTeardownStatus = "pending";
|
|
265
|
+
return {
|
|
266
|
+
promise: promise.then(
|
|
267
|
+
() => {
|
|
268
|
+
status = "settled";
|
|
269
|
+
},
|
|
270
|
+
error => {
|
|
271
|
+
status = "failed";
|
|
272
|
+
onError(error);
|
|
273
|
+
},
|
|
274
|
+
),
|
|
275
|
+
status: () => status,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Wait for cleanup only until the caller's shared absolute deadline. */
|
|
280
|
+
async function waitForVibeTeardown(tasks: readonly TrackedVibeTeardown[], deadline: number): Promise<boolean> {
|
|
281
|
+
if (tasks.length === 0 || tasks.every(task => task.status() !== "pending")) return true;
|
|
282
|
+
const remainingMs = deadline - Date.now();
|
|
283
|
+
if (remainingMs <= 0) return false;
|
|
284
|
+
const timeout = Promise.withResolvers<void>();
|
|
285
|
+
const timer = setTimeout(timeout.resolve, remainingMs);
|
|
286
|
+
timer.unref?.();
|
|
287
|
+
try {
|
|
288
|
+
return await Promise.race([
|
|
289
|
+
Promise.allSettled(tasks.map(task => task.promise)).then(() => true),
|
|
290
|
+
timeout.promise.then(() => false),
|
|
291
|
+
]);
|
|
292
|
+
} finally {
|
|
293
|
+
clearTimeout(timer);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
255
297
|
/** Normalize a text fragment to one bounded roster/trace line. */
|
|
256
298
|
function firstLine(text: string, max = 100): string {
|
|
257
299
|
return oneLineLabel(text, max);
|
|
@@ -354,24 +396,6 @@ function mergeTrace(turn: VibeTurn, progress: AgentProgress): void {
|
|
|
354
396
|
/** Thrown from a turn job body so the job manager marks the job failed while carrying the formatted result. */
|
|
355
397
|
export class VibeTurnError extends Error {}
|
|
356
398
|
|
|
357
|
-
async function awaitCancelledTurnJobs(jobs: ReadonlySet<AsyncJob>): Promise<void> {
|
|
358
|
-
if (jobs.size === 0) return;
|
|
359
|
-
const settled = Promise.allSettled([...jobs].map(job => job.promise)).then(() => true);
|
|
360
|
-
const timeout = Promise.withResolvers<false>();
|
|
361
|
-
const timer = setTimeout(() => timeout.resolve(false), CANCELLED_TURN_SETTLE_GRACE_MS);
|
|
362
|
-
timer.unref();
|
|
363
|
-
try {
|
|
364
|
-
if (!(await Promise.race([settled, timeout.promise]))) {
|
|
365
|
-
logger.warn("vibe: detached cancelled turn that did not settle within teardown grace period", {
|
|
366
|
-
jobCount: jobs.size,
|
|
367
|
-
graceMs: CANCELLED_TURN_SETTLE_GRACE_MS,
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
} finally {
|
|
371
|
-
clearTimeout(timer);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
399
|
/**
|
|
376
400
|
* Process-global registry of vibe worker sessions, scoped by both owner agent
|
|
377
401
|
* id and stable parent session id. Persisted lifecycle events rebuild idle
|
|
@@ -419,6 +443,12 @@ export class VibeSessionRegistry {
|
|
|
419
443
|
readonly #records = new Map<string, VibeRecord>();
|
|
420
444
|
readonly #terminationTails = new Map<string, Promise<void>>();
|
|
421
445
|
readonly #terminatedScopes = new Set<string>();
|
|
446
|
+
#teardownGraceMs = VIBE_TEARDOWN_GRACE_MS;
|
|
447
|
+
|
|
448
|
+
/** Override the teardown grace period for deterministic lifecycle tests. */
|
|
449
|
+
setTeardownGraceForTesting(timeoutMs: number): void {
|
|
450
|
+
this.#teardownGraceMs = Math.max(1, timeoutMs);
|
|
451
|
+
}
|
|
422
452
|
|
|
423
453
|
ownerScope(session: VibeParentSession): VibeOwnerScope {
|
|
424
454
|
const parentSessionId = session.getSessionId?.();
|
|
@@ -709,11 +739,50 @@ export class VibeSessionRegistry {
|
|
|
709
739
|
}
|
|
710
740
|
}
|
|
711
741
|
|
|
742
|
+
#trackAgentRelease(id: string, ref: AgentRef, action: "detach" | "release"): TrackedVibeTeardown {
|
|
743
|
+
return trackVibeTeardown(AgentLifecycleManager.global().release(id, ref), error => {
|
|
744
|
+
logger.warn(`vibe: failed to ${action} worker session`, {
|
|
745
|
+
id,
|
|
746
|
+
error: error instanceof Error ? error.message : String(error),
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
#finishAgentRelease(id: string, ref: AgentRef, task: TrackedVibeTeardown, action: "detach" | "release"): void {
|
|
752
|
+
if (task.status() === "settled") return;
|
|
753
|
+
if (task.status() === "pending") {
|
|
754
|
+
logger.warn(`vibe: timed out waiting to ${action} worker session; detaching registry ref`, { id });
|
|
755
|
+
}
|
|
756
|
+
AgentRegistry.global().unregister(id, ref);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async #releaseRefWithinDeadline(
|
|
760
|
+
id: string,
|
|
761
|
+
ref: AgentRef,
|
|
762
|
+
deadline: number,
|
|
763
|
+
action: "detach" | "release",
|
|
764
|
+
): Promise<void> {
|
|
765
|
+
const task = this.#trackAgentRelease(id, ref, action);
|
|
766
|
+
await waitForVibeTeardown([task], deadline);
|
|
767
|
+
this.#finishAgentRelease(id, ref, task, action);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
#trackJobSettlement(record: VibeRecord, job: AsyncJob): TrackedVibeTeardown {
|
|
771
|
+
return trackVibeTeardown(job.promise, error => {
|
|
772
|
+
logger.warn("vibe: cancelled worker turn cleanup failed", {
|
|
773
|
+
id: record.id,
|
|
774
|
+
jobId: job.id,
|
|
775
|
+
error: error instanceof Error ? error.message : String(error),
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
712
780
|
async #markTerminalRef(
|
|
713
781
|
id: string,
|
|
714
782
|
ownerId: string,
|
|
715
783
|
childSessionFile: string,
|
|
716
784
|
expected?: AgentRef | null,
|
|
785
|
+
teardownDeadline?: number,
|
|
717
786
|
): Promise<void> {
|
|
718
787
|
const registry = AgentRegistry.global();
|
|
719
788
|
const existing = registry.get(id);
|
|
@@ -726,7 +795,9 @@ export class VibeSessionRegistry {
|
|
|
726
795
|
}
|
|
727
796
|
if (existing?.status === "aborted" && !existing.session) return;
|
|
728
797
|
if (existing && !registry.setStatus(id, "aborted", existing)) return;
|
|
729
|
-
if (existing &&
|
|
798
|
+
if (existing && teardownDeadline !== undefined) {
|
|
799
|
+
await this.#releaseRefWithinDeadline(id, existing, teardownDeadline, "release");
|
|
800
|
+
} else if (existing && AgentLifecycleManager.global().has(id, existing)) {
|
|
730
801
|
await AgentLifecycleManager.global().release(id, existing);
|
|
731
802
|
} else if (existing?.session) {
|
|
732
803
|
await existing.session.dispose();
|
|
@@ -1086,21 +1157,50 @@ export class VibeSessionRegistry {
|
|
|
1086
1157
|
this.#records.delete(scopeKey(scope, record.id));
|
|
1087
1158
|
if (record.turn && manager) manager.cancel(record.turn.jobId, { ownerId: record.ownerId });
|
|
1088
1159
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1160
|
+
const deadline = Date.now() + this.#teardownGraceMs;
|
|
1161
|
+
const cleanup = teardown.map(entry => ({
|
|
1162
|
+
...entry,
|
|
1163
|
+
releaseTask: entry.ref ? this.#trackAgentRelease(entry.record.id, entry.ref, "detach") : undefined,
|
|
1164
|
+
jobTask: entry.job ? this.#trackJobSettlement(entry.record, entry.job) : undefined,
|
|
1165
|
+
}));
|
|
1166
|
+
await waitForVibeTeardown(
|
|
1167
|
+
cleanup.flatMap(entry => [entry.releaseTask, entry.jobTask].filter(task => task !== undefined)),
|
|
1168
|
+
deadline,
|
|
1169
|
+
);
|
|
1170
|
+
for (const { record, ref, releaseTask, job, jobTask } of cleanup) {
|
|
1171
|
+
if (ref && releaseTask) this.#finishAgentRelease(record.id, ref, releaseTask, "detach");
|
|
1172
|
+
if (job && jobTask?.status() === "pending") {
|
|
1173
|
+
logger.warn("vibe: timed out waiting for cancelled worker turn; cleanup continues in the background", {
|
|
1095
1174
|
id: record.id,
|
|
1096
|
-
|
|
1175
|
+
jobId: job.id,
|
|
1097
1176
|
});
|
|
1177
|
+
this.#continueSuspendedCleanup(scope, record, jobTask);
|
|
1178
|
+
}
|
|
1179
|
+
if (this.#records.has(scopeKey(scope, record.id))) continue;
|
|
1180
|
+
const lateRef = this.#registeredAgent(record);
|
|
1181
|
+
if (lateRef && lateRef !== ref) {
|
|
1182
|
+
await this.#releaseRefWithinDeadline(record.id, lateRef, deadline, "detach");
|
|
1098
1183
|
}
|
|
1099
1184
|
}
|
|
1100
|
-
await awaitCancelledTurnJobs(new Set(teardown.flatMap(entry => (entry.job ? [entry.job] : []))));
|
|
1101
1185
|
return records.length;
|
|
1102
1186
|
}
|
|
1103
1187
|
|
|
1188
|
+
#continueSuspendedCleanup(scope: VibeOwnerScope, record: VibeRecord, jobTask: TrackedVibeTeardown): void {
|
|
1189
|
+
void jobTask.promise
|
|
1190
|
+
.then(async () => {
|
|
1191
|
+
if (this.#records.has(scopeKey(scope, record.id))) return;
|
|
1192
|
+
const lateRef = this.#registeredAgent(record);
|
|
1193
|
+
if (!lateRef) return;
|
|
1194
|
+
await this.#releaseRefWithinDeadline(record.id, lateRef, Date.now() + this.#teardownGraceMs, "detach");
|
|
1195
|
+
})
|
|
1196
|
+
.catch(error => {
|
|
1197
|
+
logger.warn("vibe: failed to finish suspended worker cleanup", {
|
|
1198
|
+
id: record.id,
|
|
1199
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1104
1204
|
/** Terminate one worker; a tombstone failure still tears it down before reconciliation and error delivery. */
|
|
1105
1205
|
async kill(session: ToolSession, id: string): Promise<VibeKillOutcome> {
|
|
1106
1206
|
const scope = this.ownerScope(session);
|
|
@@ -1144,15 +1244,21 @@ export class VibeSessionRegistry {
|
|
|
1144
1244
|
await this.#persistModeExit(session, scope, records);
|
|
1145
1245
|
} catch (error) {
|
|
1146
1246
|
if (error instanceof SessionPersistenceIndeterminateError) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1247
|
+
const teardownDeadline = Date.now() + this.#teardownGraceMs;
|
|
1248
|
+
await Promise.all(
|
|
1249
|
+
records.map(record =>
|
|
1250
|
+
this.#killRecord(record, session.asyncJobManager, session, "mode-exit", false, teardownDeadline),
|
|
1251
|
+
),
|
|
1252
|
+
);
|
|
1150
1253
|
}
|
|
1151
1254
|
throw error;
|
|
1152
1255
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1256
|
+
const teardownDeadline = Date.now() + this.#teardownGraceMs;
|
|
1257
|
+
await Promise.all(
|
|
1258
|
+
records.map(record =>
|
|
1259
|
+
this.#killRecord(record, session.asyncJobManager, session, "mode-exit", true, teardownDeadline),
|
|
1260
|
+
),
|
|
1261
|
+
);
|
|
1156
1262
|
return records.length;
|
|
1157
1263
|
}
|
|
1158
1264
|
|
|
@@ -1162,6 +1268,7 @@ export class VibeSessionRegistry {
|
|
|
1162
1268
|
session: VibeParentSession,
|
|
1163
1269
|
reason: VibeTombstoneReason,
|
|
1164
1270
|
persistTerminal = true,
|
|
1271
|
+
teardownDeadline?: number,
|
|
1165
1272
|
): Promise<VibeKillOutcome> {
|
|
1166
1273
|
const registered = this.#registeredAgent(record);
|
|
1167
1274
|
const settlingJobs = new Set<AsyncJob>();
|
|
@@ -1198,26 +1305,29 @@ export class VibeSessionRegistry {
|
|
|
1198
1305
|
record.state = "dead";
|
|
1199
1306
|
record.lastActivityAt = Date.now();
|
|
1200
1307
|
record.lastActivity = "killed";
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1308
|
+
const deadline = teardownDeadline ?? Date.now() + this.#teardownGraceMs;
|
|
1309
|
+
const releaseTask = registered ? this.#trackAgentRelease(record.id, registered, "release") : undefined;
|
|
1310
|
+
const jobCleanup = [...settlingJobs].map(job => ({ job, task: this.#trackJobSettlement(record, job) }));
|
|
1311
|
+
await waitForVibeTeardown(
|
|
1312
|
+
[releaseTask, ...jobCleanup.map(entry => entry.task)].filter(task => task !== undefined),
|
|
1313
|
+
deadline,
|
|
1314
|
+
);
|
|
1315
|
+
if (registered && releaseTask) this.#finishAgentRelease(record.id, registered, releaseTask, "release");
|
|
1316
|
+
const pendingJobs = jobCleanup.filter(entry => entry.task.status() === "pending");
|
|
1317
|
+
for (const { job } of pendingJobs) {
|
|
1318
|
+
logger.warn("vibe: timed out waiting for cancelled worker turn; cleanup continues in the background", {
|
|
1319
|
+
id: record.id,
|
|
1320
|
+
jobId: job.id,
|
|
1321
|
+
});
|
|
1210
1322
|
}
|
|
1211
|
-
await awaitCancelledTurnJobs(settlingJobs);
|
|
1212
1323
|
const terminalRef = registered ?? this.#registeredAgent(record) ?? null;
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
}
|
|
1324
|
+
await this.#markTerminalRecord(record, terminalRef, deadline);
|
|
1325
|
+
if (pendingJobs.length > 0) {
|
|
1326
|
+
this.#continueKilledCleanup(
|
|
1327
|
+
record,
|
|
1328
|
+
pendingJobs.map(entry => entry.task),
|
|
1329
|
+
registered,
|
|
1330
|
+
);
|
|
1221
1331
|
}
|
|
1222
1332
|
if (persistenceError) {
|
|
1223
1333
|
let finalPersistenceError = persistenceError;
|
|
@@ -1246,6 +1356,40 @@ export class VibeSessionRegistry {
|
|
|
1246
1356
|
return { id: record.id, cancelledTurn };
|
|
1247
1357
|
}
|
|
1248
1358
|
|
|
1359
|
+
async #markTerminalRecord(
|
|
1360
|
+
record: VibeRecord,
|
|
1361
|
+
expected: AgentRef | null | undefined,
|
|
1362
|
+
teardownDeadline: number,
|
|
1363
|
+
): Promise<void> {
|
|
1364
|
+
if (!record.childSessionFile) return;
|
|
1365
|
+
try {
|
|
1366
|
+
const persisted = await SessionManager.peekSessionInit(record.childSessionFile);
|
|
1367
|
+
if (persisted?.init) {
|
|
1368
|
+
await this.#markTerminalRef(record.id, record.ownerId, record.childSessionFile, expected, teardownDeadline);
|
|
1369
|
+
}
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
logger.warn("vibe: failed to retain terminal worker transcript", {
|
|
1372
|
+
id: record.id,
|
|
1373
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
#continueKilledCleanup(
|
|
1379
|
+
record: VibeRecord,
|
|
1380
|
+
jobTasks: readonly TrackedVibeTeardown[],
|
|
1381
|
+
expected: AgentRef | undefined,
|
|
1382
|
+
): void {
|
|
1383
|
+
void Promise.allSettled(jobTasks.map(task => task.promise))
|
|
1384
|
+
.then(() => this.#markTerminalRecord(record, expected, Date.now() + this.#teardownGraceMs))
|
|
1385
|
+
.catch(error => {
|
|
1386
|
+
logger.warn("vibe: failed to finish killed worker cleanup", {
|
|
1387
|
+
id: record.id,
|
|
1388
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1389
|
+
});
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1249
1393
|
/** Build the ExecutorOptions for a first spawn, mirroring the `task`/eval-bridge plumbing. */
|
|
1250
1394
|
async #buildSpawnOptions(
|
|
1251
1395
|
session: ToolSession,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { compareVersions, tryParseJson } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
3
|
import { buildResult, loadPage } from "./types";
|
|
4
4
|
|
|
@@ -20,17 +20,6 @@ interface ParsedCabal {
|
|
|
20
20
|
stability?: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function compareVersions(a: string, b: string): number {
|
|
24
|
-
const aParts = a.split(".").map(part => Number.parseInt(part, 10) || 0);
|
|
25
|
-
const bParts = b.split(".").map(part => Number.parseInt(part, 10) || 0);
|
|
26
|
-
const max = Math.max(aParts.length, bParts.length);
|
|
27
|
-
for (let i = 0; i < max; i++) {
|
|
28
|
-
const delta = (aParts[i] || 0) - (bParts[i] || 0);
|
|
29
|
-
if (delta !== 0) return delta;
|
|
30
|
-
}
|
|
31
|
-
return 0;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
23
|
function extractCabalField(content: string, fieldName: string): string | undefined {
|
|
35
24
|
const pattern = new RegExp(`^${fieldName}:\\s*(.*)$`, "im");
|
|
36
25
|
const match = content.match(pattern);
|
|
@@ -458,7 +458,7 @@ export class ExaProvider extends SearchProvider {
|
|
|
458
458
|
* still uses {@link isAvailable} so an unrelated configured provider
|
|
459
459
|
* keeps priority over the public fallback.
|
|
460
460
|
*/
|
|
461
|
-
isExplicitlyAvailable(_authStorage: AuthStorage): boolean {
|
|
461
|
+
override isExplicitlyAvailable(_authStorage: AuthStorage): boolean {
|
|
462
462
|
return this.#settingsAllowSearch();
|
|
463
463
|
}
|
|
464
464
|
|
|
@@ -203,7 +203,7 @@ export class FirecrawlProvider extends SearchProvider {
|
|
|
203
203
|
* Firecrawl supports keyless mode, so an explicit user selection
|
|
204
204
|
* (`webSearch: firecrawl`) works without any credential configured.
|
|
205
205
|
*/
|
|
206
|
-
isExplicitlyAvailable(_authStorage: AuthStorage): boolean {
|
|
206
|
+
override isExplicitlyAvailable(_authStorage: AuthStorage): boolean {
|
|
207
207
|
return true;
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -973,7 +973,7 @@ export class PerplexityProvider extends SearchProvider {
|
|
|
973
973
|
* configured provider keeps priority over the anonymous/OpenRouter
|
|
974
974
|
* fallbacks.
|
|
975
975
|
*/
|
|
976
|
-
isExplicitlyAvailable(_authStorage: AuthStorage): boolean {
|
|
976
|
+
override isExplicitlyAvailable(_authStorage: AuthStorage): boolean {
|
|
977
977
|
return true;
|
|
978
978
|
}
|
|
979
979
|
|