termdock 1.4.311 → 1.4.312
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/dist/server/agent/agentLaunchCheck.js +22 -0
- package/dist/server/agent/collaborationCli.js +9 -0
- package/dist/server/agent/integrationServer.js +1 -0
- package/dist/server/routes/terminal.js +97 -20
- package/docs/integrations.md +15 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function normalizeAgentLaunchCheck(input) {
|
|
2
|
+
if (!input || typeof input !== 'object')
|
|
3
|
+
return null;
|
|
4
|
+
const value = input;
|
|
5
|
+
if (typeof value.id !== 'string' || !value.id || typeof value.agentSlug !== 'string' || !value.agentSlug
|
|
6
|
+
|| !Number.isFinite(value.deadline) || !(value.nativeSessionId === null || typeof value.nativeSessionId === 'string'))
|
|
7
|
+
return null;
|
|
8
|
+
return { id: value.id, agentSlug: value.agentSlug, nativeSessionId: value.nativeSessionId, deadline: value.deadline };
|
|
9
|
+
}
|
|
10
|
+
/** Caller supplies current terminal process evidence, never screen text or cached hooks. */
|
|
11
|
+
export function agentLaunchBlockReason(check, observed, now = Date.now()) {
|
|
12
|
+
if (!check)
|
|
13
|
+
return null;
|
|
14
|
+
// A late actual observation can resolve an earlier timeout without retyping.
|
|
15
|
+
if (observed.agentSlug === check.agentSlug
|
|
16
|
+
&& (!check.nativeSessionId || observed.nativeSessionId === check.nativeSessionId))
|
|
17
|
+
return null;
|
|
18
|
+
if (observed.agentSlug && observed.agentSlug !== check.agentSlug
|
|
19
|
+
|| check.nativeSessionId && observed.nativeSessionId && observed.nativeSessionId !== check.nativeSessionId)
|
|
20
|
+
return 'AGENT_LAUNCH_IDENTITY_MISMATCH';
|
|
21
|
+
return now >= check.deadline ? 'AGENT_LAUNCH_TIMEOUT' : 'AGENT_LAUNCH_PENDING';
|
|
22
|
+
}
|
|
@@ -175,6 +175,11 @@ Idempotency retention: 7 days. Terminal delivery, ACK and result never imply eac
|
|
|
175
175
|
Delivery semantics: delivered records completion of terminal write calls;
|
|
176
176
|
it does not guarantee receipt, submission, consumption or task completion; a timeout stops waiting, it does not cancel delivery.
|
|
177
177
|
Terminal input: paste once, wait 50ms, send one unbracketed CR (Enter).
|
|
178
|
+
TD-started/resumed Agents: the first delivery waits for current Agent process and
|
|
179
|
+
terminal binding (exact native id on resume); cold starts do not require a native id.
|
|
180
|
+
AGENT_LAUNCH_PENDING/TIMEOUT leaves the body queued with no terminal write.
|
|
181
|
+
This one-time launch check is not application input-ready; subsequent messages
|
|
182
|
+
and ordinary terminals follow the existing path. No screen text or hooks required.
|
|
178
183
|
No paste-marker recognition, automatic extra Enter or consumption checks.
|
|
179
184
|
The service cannot see whether the recipient's agent consumed a message.
|
|
180
185
|
Use explicit replies as evidence. A screen snapshot is only an observation;
|
|
@@ -444,6 +449,10 @@ const DELIVERY_DIAGNOSTICS = {
|
|
|
444
449
|
DELIVERY_IN_PROGRESS: '投递进行中(写入前的中间标记,正常会在数秒内推进为 delivered 或带原因的重试)',
|
|
445
450
|
TERMINAL_WRITE_FAILED: '写入对方终端失败,会按重试间隔继续尝试',
|
|
446
451
|
TMUX_PANE_CHANGED: '目标面板已变化,投递已跳过,等待重新绑定',
|
|
452
|
+
AGENT_LAUNCH_PENDING: 'TD 启动的 Agent 尚未确认运行和终端绑定;消息保留队列,尚未写入',
|
|
453
|
+
AGENT_LAUNCH_TIMEOUT: '120 秒内未观察到目标 Agent;消息保留,未盲投,不证明应用故障',
|
|
454
|
+
AGENT_LAUNCH_IDENTITY_MISMATCH: '启动后的 Agent 或恢复 UUID 不匹配;消息保留,未写入',
|
|
455
|
+
AGENT_LAUNCH_CHANGED: '启动操作已变化,重新检查当前启动记录',
|
|
447
456
|
TMUX_PANE_IN_MODE: '目标面板处于 tmux 模式(如滚动查看),按键已跳过',
|
|
448
457
|
};
|
|
449
458
|
export function deliveryDiagnosticText(lastError) {
|
|
@@ -224,6 +224,7 @@ export class IntegrationRuntime {
|
|
|
224
224
|
startup_input_conditions: true, session_runtime_release: true, stable_session_identity: true, session_lifecycle_generation: true,
|
|
225
225
|
session_resume_configuration: true, session_lifecycle_min_version: '1.4.309',
|
|
226
226
|
session_startup_input_clear: true, session_startup_input_clear_min_version: '1.4.310',
|
|
227
|
+
agent_launch_process_check: true, agent_launch_process_check_min_version: '1.4.312',
|
|
227
228
|
integration_policy_update: true,
|
|
228
229
|
session_restore_diagnostics: true,
|
|
229
230
|
native_identity_linux_flock_owner: process.platform === 'linux',
|
|
@@ -50,6 +50,7 @@ import { archiveTaskWorkspace, restoreTaskWorkspace, snapshotTaskWorkspace } fro
|
|
|
50
50
|
import { isTmuxRecoveryCandidate } from '../utils/tmuxRecoveryCandidate.js';
|
|
51
51
|
import { AutomationStore, normalizeAutomationSchedule } from '../agent/automationStore.js';
|
|
52
52
|
import { writeBracketedPrompt, canDeliverPromptToAgent } from '../agent/promptDelivery.js';
|
|
53
|
+
import { agentLaunchBlockReason, normalizeAgentLaunchCheck } from '../agent/agentLaunchCheck.js';
|
|
53
54
|
import { prepareSupervisionEnable } from '../utils/enableSupervision.js';
|
|
54
55
|
import { buildServerHealthState, dismissServerHealth } from '../utils/serverHealth.js';
|
|
55
56
|
import { collaborationRoutes } from '../agent/collaborationRoutes.js';
|
|
@@ -965,6 +966,7 @@ function normalizePersistedClientSession(input) {
|
|
|
965
966
|
? candidate.shellTitle
|
|
966
967
|
: null,
|
|
967
968
|
agentResume: normalizePersistedAgentResumeInfo(candidate.agentResume),
|
|
969
|
+
agentLaunchCheck: normalizeAgentLaunchCheck(candidate.agentLaunchCheck),
|
|
968
970
|
};
|
|
969
971
|
}
|
|
970
972
|
function generateTmuxSessionName() {
|
|
@@ -1450,15 +1452,53 @@ function searchMetadataForBackend(backendSessionId, session) {
|
|
|
1450
1452
|
function writeTerminalInput(session, value) {
|
|
1451
1453
|
session.ptyProcess.write(value.replace(/\r?\n/g, '\r'));
|
|
1452
1454
|
}
|
|
1453
|
-
function
|
|
1455
|
+
async function beginAgentLaunchCheck(frontendSessionId, agentSlug, nativeSessionId = null) {
|
|
1456
|
+
const record = globalSessionState.sessions.find(candidate => candidate.sessionId === frontendSessionId);
|
|
1457
|
+
if (!record)
|
|
1458
|
+
throw new Error('SESSION_REMOVED');
|
|
1459
|
+
upsertGlobalSessionRecord({ ...record, agentLaunchCheck: { id: randomUUID(), agentSlug, nativeSessionId, deadline: Date.now() + 120_000 } });
|
|
1460
|
+
await persistGlobalStateNow();
|
|
1461
|
+
}
|
|
1462
|
+
async function observeAgentLaunchCheck(record, observed) {
|
|
1463
|
+
const check = record.agentLaunchCheck;
|
|
1464
|
+
if (!check)
|
|
1465
|
+
return null;
|
|
1466
|
+
const reason = agentLaunchBlockReason(check, observed);
|
|
1467
|
+
if (reason)
|
|
1468
|
+
return { state: 'recovering', reason };
|
|
1469
|
+
const latest = globalSessionState.sessions.find(candidate => candidate.sessionId === record.sessionId);
|
|
1470
|
+
if (!latest)
|
|
1471
|
+
return { state: 'offline', reason: 'SESSION_REMOVED' };
|
|
1472
|
+
if (latest.agentLaunchCheck?.id !== check.id)
|
|
1473
|
+
return { state: 'recovering', reason: 'AGENT_LAUNCH_CHANGED' };
|
|
1474
|
+
upsertGlobalSessionRecord({ ...latest, agentLaunchCheck: null });
|
|
1475
|
+
await persistGlobalStateNow();
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
async function deliverAutomationPromptWhenReady(backendSessionId, prompt, attempt = 0) {
|
|
1454
1479
|
const session = terminalSessions.get(backendSessionId);
|
|
1455
1480
|
if (!session)
|
|
1456
1481
|
return;
|
|
1482
|
+
const record = globalSessionState.sessions.find(candidate => candidate.backendSessionId === backendSessionId);
|
|
1483
|
+
if (record?.agentLaunchCheck) {
|
|
1484
|
+
await refreshCollaborationAgentIdentity(backendSessionId, session, true);
|
|
1485
|
+
const agent = detectAgentFromCommand(session.activeProgram?.rawArgs ?? session.activeProgram?.command ?? '', agentCustomCommands());
|
|
1486
|
+
const nativeSessionId = agent && session.activeProgram?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(session.activeProgram.rawArgs)) : null;
|
|
1487
|
+
const blocked = await observeAgentLaunchCheck(record, { agentSlug: agent?.slug ?? null, nativeSessionId });
|
|
1488
|
+
if (blocked) {
|
|
1489
|
+
if (blocked.reason !== 'AGENT_LAUNCH_PENDING')
|
|
1490
|
+
throw new Error(blocked.reason ?? 'AGENT_LAUNCH_PENDING');
|
|
1491
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1492
|
+
return deliverAutomationPromptWhenReady(backendSessionId, prompt, attempt + 1);
|
|
1493
|
+
}
|
|
1494
|
+
await writeBracketedPrompt(session.ptyProcess, prompt);
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1457
1497
|
if (canDeliverPromptToAgent(session) || attempt >= 60) {
|
|
1458
1498
|
void writeBracketedPrompt(session.ptyProcess, prompt).catch((error) => console.error('[automation] terminal write failed:', getErrorMessage(error)));
|
|
1459
1499
|
return;
|
|
1460
1500
|
}
|
|
1461
|
-
setTimeout(() => deliverAutomationPromptWhenReady(backendSessionId, prompt, attempt + 1), 250);
|
|
1501
|
+
setTimeout(() => { void deliverAutomationPromptWhenReady(backendSessionId, prompt, attempt + 1).catch(error => console.error('[automation] terminal write failed:', getErrorMessage(error))); }, 250);
|
|
1462
1502
|
}
|
|
1463
1503
|
async function runAgentAutomation(automation, req) {
|
|
1464
1504
|
const run = automationStore.beginRun(automation);
|
|
@@ -1469,13 +1509,16 @@ async function runAgentAutomation(automation, req) {
|
|
|
1469
1509
|
const backend = record?.backendSessionId ? terminalSessions.get(record.backendSessionId) : null;
|
|
1470
1510
|
if (!record || !backend)
|
|
1471
1511
|
throw new Error('目标会话当前不在线');
|
|
1472
|
-
if (!canDeliverPromptToAgent(backend))
|
|
1512
|
+
if (!record.agentLaunchCheck && !canDeliverPromptToAgent(backend))
|
|
1473
1513
|
throw new Error('目标会话当前没有运行中的 Agent');
|
|
1474
1514
|
frontendSessionId = record.sessionId;
|
|
1475
1515
|
const message = automation.prompt || automation.command;
|
|
1476
1516
|
if (!message)
|
|
1477
1517
|
throw new Error('自动任务没有可发送的内容');
|
|
1478
|
-
|
|
1518
|
+
if (record.agentLaunchCheck)
|
|
1519
|
+
await deliverAutomationPromptWhenReady(record.backendSessionId, message);
|
|
1520
|
+
else
|
|
1521
|
+
await writeBracketedPrompt(backend.ptyProcess, message);
|
|
1479
1522
|
}
|
|
1480
1523
|
else {
|
|
1481
1524
|
const request = req ?? {};
|
|
@@ -1489,11 +1532,15 @@ async function runAgentAutomation(automation, req) {
|
|
|
1489
1532
|
const backend = terminalSessions.get(opened.terminalSession.sessionId);
|
|
1490
1533
|
if (!backend)
|
|
1491
1534
|
throw new Error('自动任务会话创建失败');
|
|
1492
|
-
if (automation.command)
|
|
1535
|
+
if (automation.command) {
|
|
1536
|
+
const agent = detectAgentFromCommand(automation.command, agentCustomCommands());
|
|
1537
|
+
if (agent)
|
|
1538
|
+
await beginAgentLaunchCheck(frontendSessionId, agent.slug);
|
|
1493
1539
|
writeTerminalInput(backend, `${automation.command}\r`);
|
|
1540
|
+
}
|
|
1494
1541
|
if (automation.prompt) {
|
|
1495
1542
|
if (automation.command)
|
|
1496
|
-
deliverAutomationPromptWhenReady(opened.terminalSession.sessionId, automation.prompt);
|
|
1543
|
+
await deliverAutomationPromptWhenReady(opened.terminalSession.sessionId, automation.prompt);
|
|
1497
1544
|
else
|
|
1498
1545
|
await writeBracketedPrompt(backend.ptyProcess, automation.prompt);
|
|
1499
1546
|
}
|
|
@@ -1580,7 +1627,7 @@ function formatLocalCollaborationMessages(frontendSessionId, messages) {
|
|
|
1580
1627
|
* agent-keyed selector can never reach it. Delivery still needs an agent (the
|
|
1581
1628
|
* confirm gate and prompt formatting assume a TUI); driving a terminal —
|
|
1582
1629
|
* run/capture — is a shell operation and works on any pane. */
|
|
1583
|
-
async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane = false, nativeProcessIdentity = false) {
|
|
1630
|
+
async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane = false, nativeProcessIdentity = false, freshProcess = false) {
|
|
1584
1631
|
if (!binding.tmuxSessionName)
|
|
1585
1632
|
return { state: 'offline', reason: 'TMUX_BINDING_MISSING' };
|
|
1586
1633
|
const name = `=${binding.tmuxSessionName}`;
|
|
@@ -1599,12 +1646,12 @@ async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane =
|
|
|
1599
1646
|
const serverPid = Number((await runTmux(['display-message', '-p', '#{pid}'])).trim());
|
|
1600
1647
|
if (!Number.isInteger(serverPid) || serverPid <= 0)
|
|
1601
1648
|
throw new Error('TMUX_SERVER_IDENTITY_UNAVAILABLE');
|
|
1602
|
-
if (nativeProcessIdentity && process.platform === 'linux')
|
|
1649
|
+
if ((nativeProcessIdentity || freshProcess) && process.platform === 'linux')
|
|
1603
1650
|
await getProcessSnapshot(true);
|
|
1604
1651
|
const panes = await Promise.all(layout.windows.flatMap((window) => window.panes).map(async (pane) => {
|
|
1605
1652
|
// Native identity needs the foreground argv even for a named binary;
|
|
1606
1653
|
// pane_current_command alone cannot prove an exact resumed conversation.
|
|
1607
|
-
const program = await resolveTmuxPaneProgram(pane, true);
|
|
1654
|
+
const program = await resolveTmuxPaneProgram(pane, true, nativeProcessIdentity || freshProcess);
|
|
1608
1655
|
const agent = detectAgentFromCommand(program?.rawArgs ?? program?.command ?? '', agentCustomCommands());
|
|
1609
1656
|
const identity = nativeProcessIdentity ? await observeProgramNativeIdentity(agent, program)
|
|
1610
1657
|
: { nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null, failure: null };
|
|
@@ -1664,8 +1711,8 @@ async function rebindCollaborationRoute(frontendSessionId, paneId) {
|
|
|
1664
1711
|
return { ...collaborationRouting.get(frontendSessionId), state: 'recovering' };
|
|
1665
1712
|
}
|
|
1666
1713
|
/**
|
|
1667
|
-
*
|
|
1668
|
-
*
|
|
1714
|
+
* A TD-initiated Agent launch waits once for current process/binding evidence.
|
|
1715
|
+
* Existing terminals receive messages whatever their agent is doing. Busy guessing (hook state,
|
|
1669
1716
|
* turn phase) is the sender's job — the send receipt carries a screen
|
|
1670
1717
|
* snapshot of the recipient after the write, and the sender judges from that.
|
|
1671
1718
|
*/
|
|
@@ -1684,13 +1731,16 @@ async function resolveCollaborationRoute(frontendSessionId) {
|
|
|
1684
1731
|
agentSlug: record.agentResume?.slug ?? null, nativeSessionId: record.agentResume?.sessionId ?? null, pane: null,
|
|
1685
1732
|
};
|
|
1686
1733
|
if (binding.mode === 'tmux') {
|
|
1687
|
-
const inspected = await inspectCollaborationTmux(binding);
|
|
1734
|
+
const inspected = await inspectCollaborationTmux(binding, undefined, false, Boolean(record.agentLaunchCheck?.nativeSessionId), Boolean(record.agentLaunchCheck));
|
|
1688
1735
|
if (inspected.state !== 'ready' || !inspected.pane)
|
|
1689
1736
|
return inspected;
|
|
1690
1737
|
record = globalSessionState.sessions.find((candidate) => candidate.sessionId === frontendSessionId);
|
|
1691
1738
|
if (!record)
|
|
1692
1739
|
return { state: 'offline', reason: 'SESSION_REMOVED_DURING_RECOVERY' };
|
|
1693
1740
|
const pane = inspected.pane;
|
|
1741
|
+
const launch = await observeAgentLaunchCheck(record, { agentSlug: pane.agentSlug || null, nativeSessionId: pane.nativeSessionId });
|
|
1742
|
+
if (launch)
|
|
1743
|
+
return launch;
|
|
1694
1744
|
// Remember the target before attachment. A failed attachment must not let
|
|
1695
1745
|
// a subsequent active-pane change silently choose another recipient.
|
|
1696
1746
|
const pinned = { serverPid: pane.serverPid, sessionId: pane.sessionId, paneId: pane.paneId, panePid: pane.panePid,
|
|
@@ -1726,8 +1776,15 @@ async function resolveCollaborationRoute(frontendSessionId) {
|
|
|
1726
1776
|
}
|
|
1727
1777
|
if (!backend || !binding.backendSessionId)
|
|
1728
1778
|
return { state: 'offline', reason: 'SHELL_BACKEND_NOT_RUNNING' };
|
|
1729
|
-
await refreshCollaborationAgentIdentity(binding.backendSessionId, backend);
|
|
1779
|
+
await refreshCollaborationAgentIdentity(binding.backendSessionId, backend, Boolean(record.agentLaunchCheck));
|
|
1730
1780
|
const target = backend;
|
|
1781
|
+
if (record.agentLaunchCheck) {
|
|
1782
|
+
const agent = detectAgentFromCommand(target.activeProgram?.rawArgs ?? target.activeProgram?.command ?? '', agentCustomCommands());
|
|
1783
|
+
const nativeSessionId = agent && target.activeProgram?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(target.activeProgram.rawArgs)) : null;
|
|
1784
|
+
const launch = await observeAgentLaunchCheck(record, { agentSlug: agent?.slug ?? null, nativeSessionId });
|
|
1785
|
+
if (launch)
|
|
1786
|
+
return launch;
|
|
1787
|
+
}
|
|
1731
1788
|
return { state: 'ready',
|
|
1732
1789
|
isShell: backend.activeProgram?.source !== 'unknown' && shellNamesBackend.has(backend.activeProgram?.command?.toLowerCase() ?? ''),
|
|
1733
1790
|
capture: async () => target.autoTitleContext,
|
|
@@ -1740,15 +1797,17 @@ async function resolveCollaborationRoute(frontendSessionId) {
|
|
|
1740
1797
|
target.lastActivity = Date.now();
|
|
1741
1798
|
} };
|
|
1742
1799
|
}
|
|
1743
|
-
async function refreshCollaborationAgentIdentity(backendSessionId, session) {
|
|
1800
|
+
async function refreshCollaborationAgentIdentity(backendSessionId, session, freshProcess = false) {
|
|
1744
1801
|
let activeProgram = null;
|
|
1745
1802
|
if (session.mode === 'shell') {
|
|
1746
|
-
activeProgram = await getCachedShellActiveProgram(backendSessionId, session);
|
|
1803
|
+
activeProgram = freshProcess ? await detectShellActiveProgram(session, true) : await getCachedShellActiveProgram(backendSessionId, session);
|
|
1747
1804
|
}
|
|
1748
1805
|
else if (session.tmuxSessionName) {
|
|
1749
|
-
const layout = await getCachedTmuxLayout(session.tmuxSessionName);
|
|
1806
|
+
const layout = freshProcess ? await getTmuxLayout(session.tmuxSessionName) : await getCachedTmuxLayout(session.tmuxSessionName);
|
|
1807
|
+
if (freshProcess && process.platform === 'linux')
|
|
1808
|
+
await getProcessSnapshot(true);
|
|
1750
1809
|
const activePane = getActivePaneFromLayout(layout);
|
|
1751
|
-
const resolved = activePane ? await resolveTmuxPaneProgram(activePane) : null;
|
|
1810
|
+
const resolved = activePane ? await resolveTmuxPaneProgram(activePane, freshProcess, freshProcess) : null;
|
|
1752
1811
|
activeProgram = resolved
|
|
1753
1812
|
? { ...resolved, updatedAt: Date.now() }
|
|
1754
1813
|
: getActiveProgramFromTmuxLayout(layout);
|
|
@@ -1792,6 +1851,7 @@ async function spawnCollaborationAgentSession(req, group, sourceSessionId, input
|
|
|
1792
1851
|
const backend = terminalSessions.get(opened.terminalSession.sessionId);
|
|
1793
1852
|
if (!backend)
|
|
1794
1853
|
throw new HttpStatusError(500, 'Agent Session 创建失败', 'COLLAB_AGENT_SESSION_FAILED');
|
|
1854
|
+
await beginAgentLaunchCheck(frontendSessionId, launcher.slug);
|
|
1795
1855
|
const latestGroup = collaborationStore.getGroup(group.id);
|
|
1796
1856
|
if (!latestGroup || latestGroup.deleted) {
|
|
1797
1857
|
const created = globalSessionState.sessions.find(record => record.sessionId === frontendSessionId);
|
|
@@ -2096,6 +2156,7 @@ async function launchIntegrationTerminal(record, restoring, prepared) {
|
|
|
2096
2156
|
backend.agentSession = null;
|
|
2097
2157
|
backend.agent = null;
|
|
2098
2158
|
backend.agentResumeRecovered = false;
|
|
2159
|
+
await beginAgentLaunchCheck(record.session_id, record.agent_slug, restoring ? record.agent_native_session_id : null);
|
|
2099
2160
|
await runTmux(['send-keys', '-t', pinned.paneId, '-l', integrationLaunchCommand(record, restoring)]);
|
|
2100
2161
|
await runTmux(['send-keys', '-t', pinned.paneId, 'Enter']);
|
|
2101
2162
|
broadcastClientState();
|
|
@@ -2144,6 +2205,7 @@ export function prepareCollaborationTaskWorker(task, template, dependencies, ser
|
|
|
2144
2205
|
throw error;
|
|
2145
2206
|
}
|
|
2146
2207
|
if (!currentAgent && (!launch || launch.backendId !== opened.terminalSession.sessionId)) {
|
|
2208
|
+
await beginAgentLaunchCheck(record.sessionId, launcher.slug);
|
|
2147
2209
|
// Persist before typing: a retry must never type the launcher into a newly booted Agent's prompt.
|
|
2148
2210
|
const temporary = `${launchFile}.${process.pid}.tmp`;
|
|
2149
2211
|
await fs.promises.writeFile(temporary, JSON.stringify({ sessionId: record.sessionId, backendId: opened.terminalSession.sessionId }), { mode: 0o600 });
|
|
@@ -4247,7 +4309,7 @@ async function sampleTerminalSession(sessionId, session) {
|
|
|
4247
4309
|
events.push({ type: 'tmux-layout', layout });
|
|
4248
4310
|
return events;
|
|
4249
4311
|
}
|
|
4250
|
-
async function detectShellActiveProgram(session) {
|
|
4312
|
+
async function detectShellActiveProgram(session, includeArguments = false) {
|
|
4251
4313
|
const pid = getPtyProcessPid(session.ptyProcess);
|
|
4252
4314
|
if (pid === null) {
|
|
4253
4315
|
return null;
|
|
@@ -4286,10 +4348,15 @@ async function detectShellActiveProgram(session) {
|
|
|
4286
4348
|
const foregroundRows = rows.filter((row) => row.command && row.tpgid > 0 && row.pgid === row.tpgid && !row.stat.startsWith('Z'));
|
|
4287
4349
|
const preferredForeground = foregroundRows.find((row) => row.pid !== pid) ?? foregroundRows[foregroundRows.length - 1];
|
|
4288
4350
|
if (preferredForeground?.command) {
|
|
4351
|
+
// Startup checks need current argv, including an exact resume UUID;
|
|
4352
|
+
// ordinary terminal sampling retains its existing inexpensive path.
|
|
4353
|
+
const rawArgs = includeArguments
|
|
4354
|
+
? (await execFileAsync('ps', ['-p', String(preferredForeground.pid), '-o', 'args='], { timeout: 3000, maxBuffer: 512 * 1024 })).stdout.trim() || null
|
|
4355
|
+
: null;
|
|
4289
4356
|
return {
|
|
4290
4357
|
command: preferredForeground.command,
|
|
4291
4358
|
source: 'shell-tty',
|
|
4292
|
-
rawArgs
|
|
4359
|
+
rawArgs,
|
|
4293
4360
|
updatedAt: Date.now(),
|
|
4294
4361
|
};
|
|
4295
4362
|
}
|
|
@@ -6752,6 +6819,7 @@ router.post('/operations/execution-archives/:sessionId/restore', executionArchiv
|
|
|
6752
6819
|
broadcastClientState();
|
|
6753
6820
|
throw error;
|
|
6754
6821
|
}
|
|
6822
|
+
await beginAgentLaunchCheck(record.sessionId, target.slug, target.nativeSessionId);
|
|
6755
6823
|
writeTerminalInput(backend, `${command}\r`);
|
|
6756
6824
|
restoreArchivedMembership(entry, record.sessionId);
|
|
6757
6825
|
await persistGlobalStateNow();
|
|
@@ -7990,7 +8058,16 @@ router.post('/:sessionId/agent-resume', async (req, res) => {
|
|
|
7990
8058
|
});
|
|
7991
8059
|
}
|
|
7992
8060
|
try {
|
|
7993
|
-
|
|
8061
|
+
const frontend = globalSessionState.sessions.find(candidate => candidate.backendSessionId === sessionId);
|
|
8062
|
+
const launch = async () => {
|
|
8063
|
+
if (frontend)
|
|
8064
|
+
await beginAgentLaunchCheck(frontend.sessionId, target.slug, target.nativeSessionId);
|
|
8065
|
+
await writeBracketedPrompt(session.ptyProcess, target.command);
|
|
8066
|
+
};
|
|
8067
|
+
if (frontend)
|
|
8068
|
+
await collaborationDeliveryWorker.reconfigure(frontend.sessionId, launch);
|
|
8069
|
+
else
|
|
8070
|
+
await launch();
|
|
7994
8071
|
session.lastActivity = Date.now();
|
|
7995
8072
|
res.json({ success: true, command: target.command });
|
|
7996
8073
|
}
|
package/docs/integrations.md
CHANGED
|
@@ -504,6 +504,21 @@ session 事件;get 也会现场核验。120 秒内未形成绑定返回 `SESSI
|
|
|
504
504
|
不自动补回车、不根据终端文字确认消费。`delivered`/`pty_written` 是现有回执字段,
|
|
505
505
|
仅记录终端写入调用完成,不保证对方收到、提交、消费或执行。快照只作观测,明确回复单独关联。
|
|
506
506
|
旧回执中的 `AGENT_CONSUME_UNCONFIRMED` 保留历史,不触发补键或重投。
|
|
507
|
+
|
|
508
|
+
1.4.312 起,TD 实际启动/恢复 Agent 时持久记录一次进程检查意图。仅该次启动的
|
|
509
|
+
首次投递在原终端观察到目标 Agent 后放行;恢复还须匹配原 native UUID,冷启动
|
|
510
|
+
不要求首次输入前尚未生成的 UUID。已有会话和普通 Terminal 不新增检查;运行中
|
|
511
|
+
no-op restore 不重新开启检查。检查是运行/绑定事实,不表示 TUI 已完成输入初始化。
|
|
512
|
+
`startup_input_disabled` 仅清除画面条件,独立于该进程检查。capabilities 为
|
|
513
|
+
`agent_launch_process_check=true`,最低运行服务 1.4.312;建议 CLI 同升以取得新诊断文案。
|
|
514
|
+
|
|
515
|
+
未确认时普通消息保持 pending、实际写入次数不增加,诊断 `AGENT_LAUNCH_PENDING`;
|
|
516
|
+
120 秒后 `AGENT_LAUNCH_TIMEOUT`,不同 Agent/原 UUID 为 `AGENT_LAUNCH_IDENTITY_MISMATCH`。
|
|
517
|
+
不因超时盲投或重新启动,后续如果现场确实观察到正确进程可解除检查。检查随活动
|
|
518
|
+
终端登记持久化,服务重启保留;新的实际启动/恢复重新生成检查 ID,旧观察不能清除
|
|
519
|
+
新检查。一旦当前检查解除,后续普通消息沿原终端路由,不重复判断应用就绪/忙闲。
|
|
520
|
+
已正常 release 的承载登记被移除,下一次 restore 在新承载上重新记录检查。
|
|
521
|
+
|
|
507
522
|
任务完成继续以负责人明确 `report complete` 为准。
|
|
508
523
|
|
|
509
524
|
恢复保留原 TD ID、原生 ID、argv/model/cwd/包装器配置。原会话已经精确运行时直接
|
package/package.json
CHANGED
package/runtime-manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "termdock",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.312",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
8
|
"dependencyHash": "sha256-oaGYi/P5ob8sd6VnNBOwshKozjhdV+hYiCmLAsn5x00=",
|
|
9
|
-
"serverBundleHash": "sha256-
|
|
9
|
+
"serverBundleHash": "sha256-3z8IRTcO44/bc2KIMkaUKGeZbMd7loehrVjqv4I0pGI=",
|
|
10
10
|
"clientBundleHash": "sha256-rSq/8EDPH8i6WLa9SUHpZCeR4b5vZUPiTUusapoxjBs=",
|
|
11
11
|
"entrypoint": "dist/server/cli.js",
|
|
12
12
|
"clientEntrypoint": "dist/client/index.html"
|