|
|
@@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
|
import z from '@deepseek-ai/schemastery'
|
|
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
|
-import type { TerminalReadResult, TerminalSendResult, TerminalSessionId } from '@deepseek-ai/dsh-terminal'
|
|
|
+import type { TerminalReadResult, TerminalSessionId } from '@deepseek-ai/dsh-terminal'
|
|
|
import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
|
|
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
|
|
|
@@ -15,7 +15,6 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
const TRUNCATED_MESSAGE = '<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>'
|
|
|
const LOST_PREFIX_MESSAGE = '<response clipped><NOTE>The beginning of this command output was dropped by the terminal scrollback limit. The following text is the earliest retained output.</NOTE>\n'
|
|
|
const SHELL_RESET_MESSAGE = 'The persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment.'
|
|
|
-const SHELL_PROMPT = '__DSH_PERSISTENT_BASH_PROMPT__ '
|
|
|
const TIMEOUT_CODE = 'PERSISTENT_BASH_TIMEOUT'
|
|
|
// One page is enough to find a just-emitted completion marker; the full
|
|
|
// scrollback is assembled only when a command settles or needs partial output.
|
|
|
@@ -82,12 +81,8 @@ function wrapCommand(command: string, marker: CommandMarkers): string {
|
|
|
return `printf '%s\\n' ${quoteForBash(marker.start)}; eval -- ${quoteForBash(command)}; __dsh_persistent_bash_status=$?; printf '%s%s\\n' ${quoteForBash(marker.end)} "$__dsh_persistent_bash_status"`
|
|
|
}
|
|
|
|
|
|
-function stripPrompt(text: string): string {
|
|
|
- let result = text.replace(/\r?\n$/, '')
|
|
|
- while (result.endsWith(SHELL_PROMPT)) {
|
|
|
- result = result.slice(0, -SHELL_PROMPT.length)
|
|
|
- }
|
|
|
- return result.endsWith('\n') ? result.slice(0, -1) : result
|
|
|
+function trimTrailingNewline(text: string): string {
|
|
|
+ return text.replace(/\r?\n$/, '')
|
|
|
}
|
|
|
|
|
|
function commandOutput(
|
|
|
@@ -101,18 +96,12 @@ function commandOutput(
|
|
|
const startMarker = text.lastIndexOf(marker.start, end)
|
|
|
const start = startMarker < 0 ? 0 : startMarker + marker.start.length
|
|
|
return {
|
|
|
- text: stripPrompt(text.slice(start, end).replace(/^\r?\n/, '')),
|
|
|
+ text: trimTrailingNewline(text.slice(start, end).replace(/^\r?\n/, '')),
|
|
|
incomplete: startMarker < 0,
|
|
|
exitCode: Number(status),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function promptCompleted(result: TerminalSendResult): boolean {
|
|
|
- return result.viewport.endsWith(SHELL_PROMPT)
|
|
|
- || result.viewport.endsWith(`${SHELL_PROMPT}\r\n`)
|
|
|
- || result.viewport.endsWith(`${SHELL_PROMPT}\n`)
|
|
|
-}
|
|
|
-
|
|
|
function partialOutput(
|
|
|
snapshot: RetainedOutput,
|
|
|
marker: CommandMarkers,
|
|
|
@@ -122,7 +111,7 @@ function partialOutput(
|
|
|
const startMarker = snapshot.text.lastIndexOf(marker.start)
|
|
|
if (startMarker >= 0) {
|
|
|
return {
|
|
|
- text: stripPrompt(snapshot.text.slice(startMarker + marker.start.length).replace(/^\r?\n/, '')),
|
|
|
+ text: trimTrailingNewline(snapshot.text.slice(startMarker + marker.start.length).replace(/^\r?\n/, '')),
|
|
|
incomplete: false,
|
|
|
}
|
|
|
}
|
|
|
@@ -133,7 +122,7 @@ function partialOutput(
|
|
|
const fallbackEnd = afterStart.lastIndexOf(marker.end)
|
|
|
const beforeEnd = fallbackEnd < 0 ? afterStart : afterStart.slice(0, fallbackEnd)
|
|
|
return {
|
|
|
- text: stripPrompt(beforeEnd.replaceAll(SHELL_PROMPT, '')),
|
|
|
+ text: trimTrailingNewline(beforeEnd),
|
|
|
incomplete: fallbackTruncated || fallbackStart < 0,
|
|
|
}
|
|
|
}
|
|
|
@@ -243,8 +232,10 @@ function persistentShells(ctx: Context, config: ResolvedConfig): PersistentShell
|
|
|
live.delete(owner)
|
|
|
}, 'tool-bash-persistent owner cache cleanup')
|
|
|
}
|
|
|
+ // Echo suppression only: the prompt stays the backend's own, so the
|
|
|
+ // backend's prompt-based readiness detection keeps working.
|
|
|
const setup = ctx.terminals.startSend(owner, spawned.sessionId, {
|
|
|
- text: `stty -echo; PS1=${quoteForBash(SHELL_PROMPT)}`,
|
|
|
+ text: 'stty -echo',
|
|
|
submit: true,
|
|
|
signal: combinedSignal,
|
|
|
})
|
|
|
@@ -339,7 +330,11 @@ async function executeCommand(
|
|
|
SHELL_RESET_MESSAGE,
|
|
|
].filter(part => part.length > 0).join('\n')
|
|
|
}
|
|
|
- if (promptCompleted(result)) {
|
|
|
+ // The shell reads stdin again (its prompt, or a foreground child's own
|
|
|
+ // read) without having printed the end marker — e.g. `exec`, an interrupt,
|
|
|
+ // or an interactive child. Return what was captured instead of spinning
|
|
|
+ // until the command deadline.
|
|
|
+ if (result.waitReason === 'stdin_read') {
|
|
|
const snapshot = retainedScrollback(ctx, owner, id, latest)
|
|
|
return renderCaptured(
|
|
|
partialOutput(snapshot, marker, fallback, fallbackTruncated),
|