termdock 1.4.302 → 1.4.304
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.
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { integrationError } from './integrationStore.js';
|
|
2
|
+
/** A tmux remains observable without a browser/PTY attachment, including after
|
|
3
|
+
* service restart. Inventory UUIDs never filter the candidate set. */
|
|
4
|
+
export function collectNativeResumeOwnerCandidates(inventory, backends) {
|
|
5
|
+
const candidates = [];
|
|
6
|
+
const seenTmux = new Set(), seenBackend = new Set();
|
|
7
|
+
for (const entry of inventory) {
|
|
8
|
+
if (entry.mode !== 'tmux' || !entry.tmuxSessionName || seenTmux.has(entry.tmuxSessionName))
|
|
9
|
+
continue;
|
|
10
|
+
const backend = entry.backendSessionId ? backends.get(entry.backendSessionId) : null;
|
|
11
|
+
candidates.push({ backendSessionId: entry.backendSessionId ?? `inventory:${entry.sessionId}`,
|
|
12
|
+
tmuxSessionName: entry.tmuxSessionName, cachedSlug: backend?.agent?.slug ?? entry.agentResume?.slug ?? null,
|
|
13
|
+
cachedNativeId: backend?.agentSession?.sessionId ?? entry.agentResume?.sessionId ?? null });
|
|
14
|
+
seenTmux.add(entry.tmuxSessionName);
|
|
15
|
+
if (entry.backendSessionId)
|
|
16
|
+
seenBackend.add(entry.backendSessionId);
|
|
17
|
+
}
|
|
18
|
+
for (const [backendSessionId, backend] of backends) {
|
|
19
|
+
if (seenBackend.has(backendSessionId) || backend.mode === 'tmux' && backend.tmuxSessionName && seenTmux.has(backend.tmuxSessionName))
|
|
20
|
+
continue;
|
|
21
|
+
candidates.push({ backendSessionId, tmuxSessionName: backend.mode === 'tmux' ? backend.tmuxSessionName : null,
|
|
22
|
+
cachedSlug: backend.agent?.slug ?? null, cachedNativeId: backend.agentSession?.sessionId ?? null });
|
|
23
|
+
if (backend.mode === 'tmux' && backend.tmuxSessionName)
|
|
24
|
+
seenTmux.add(backend.tmuxSessionName);
|
|
25
|
+
}
|
|
26
|
+
return candidates;
|
|
27
|
+
}
|
|
28
|
+
/** Only process observations prove presence or absence of another owner.
|
|
29
|
+
* Cached UUIDs cannot exclude a terminal; the caller verifies its own pane. */
|
|
30
|
+
export async function assertNativeResumeAvailable(target, excludedBackendSessionId, candidates, observe) {
|
|
31
|
+
let uncertain = false;
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
if (candidate.backendSessionId === excludedBackendSessionId)
|
|
34
|
+
continue;
|
|
35
|
+
let processes;
|
|
36
|
+
try {
|
|
37
|
+
processes = await observe(candidate);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
uncertain = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (const process of processes) {
|
|
44
|
+
if (process.confirmed && process.agentSlug === target.slug && process.nativeId === target.nativeSessionId) {
|
|
45
|
+
integrationError('NATIVE_SESSION_ALREADY_RUNNING', 'Another terminal process proves the exact native conversation is running', 409);
|
|
46
|
+
}
|
|
47
|
+
if (!process.confirmed || process.agentSlug === target.slug && !process.nativeId)
|
|
48
|
+
uncertain = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (uncertain)
|
|
52
|
+
integrationError('NATIVE_SESSION_OWNER_UNCONFIRMED', 'A possible native owner cannot be verified; inspect its terminal before restoring', 409);
|
|
53
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { collaborationSessionDisplayName } from '../utils/sessionDisplayName.js';
|
|
2
2
|
import { integrationError } from '../agent/integrationStore.js';
|
|
3
|
+
import { assertNativeResumeAvailable, collectNativeResumeOwnerCandidates } from '../agent/nativeResumeOwner.js';
|
|
3
4
|
import { integrationLaunchCommand } from '../agent/integrationSessions.js';
|
|
4
5
|
import { ensureTeam } from '../agent/collaborationTeam.js';
|
|
5
6
|
import { androidRecordings } from '../android/recording.js';
|
|
@@ -1899,11 +1900,52 @@ export const integrationSessionAdapter = {
|
|
|
1899
1900
|
const agent = agentBySlug(record.agent_slug);
|
|
1900
1901
|
if (!record.agent_native_session_id || !agent || !buildResumeCommand(agent, record.agent_native_session_id, null))
|
|
1901
1902
|
integrationError('EXACT_RESUME_UNSUPPORTED', 'Agent plugin must support exact native resume');
|
|
1902
|
-
|
|
1903
|
-
integrationError('NATIVE_SESSION_ALREADY_RUNNING', 'Native Agent conversation is already running', 409);
|
|
1903
|
+
await assertIntegrationNativeResumeAvailable(record);
|
|
1904
1904
|
await launchIntegrationTerminal(record, true, prepared);
|
|
1905
1905
|
},
|
|
1906
1906
|
};
|
|
1907
|
+
async function assertIntegrationNativeResumeAvailable(record) {
|
|
1908
|
+
const own = integrationTerminalRecord(record);
|
|
1909
|
+
// Refresh the shared Linux process snapshot once for this restore decision.
|
|
1910
|
+
// Never use a recovered hook or the last-known UUID as live-owner proof.
|
|
1911
|
+
if (process.platform === 'linux')
|
|
1912
|
+
await getProcessSnapshot(true);
|
|
1913
|
+
const candidates = collectNativeResumeOwnerCandidates(globalSessionState.sessions, terminalSessions);
|
|
1914
|
+
await assertNativeResumeAvailable({ slug: record.agent_slug, nativeSessionId: record.agent_native_session_id }, own?.backendSessionId ?? null, candidates, async (candidate) => {
|
|
1915
|
+
const backend = terminalSessions.get(candidate.backendSessionId);
|
|
1916
|
+
const tmuxSessionName = candidate.tmuxSessionName;
|
|
1917
|
+
// Duplicate inventory attachments to the original owned pane are not
|
|
1918
|
+
// another process; launchIntegrationTerminal verifies that pane again.
|
|
1919
|
+
if (tmuxSessionName && tmuxSessionName === own?.tmuxSessionName)
|
|
1920
|
+
return [];
|
|
1921
|
+
if (tmuxSessionName) {
|
|
1922
|
+
try {
|
|
1923
|
+
await runTmux(['has-session', '-t', `=${tmuxSessionName}`]);
|
|
1924
|
+
}
|
|
1925
|
+
catch (error) {
|
|
1926
|
+
if (/can't find session|no server running|no sessions|error connecting.*No such file/i.test(getErrorMessage(error)))
|
|
1927
|
+
return [];
|
|
1928
|
+
throw error;
|
|
1929
|
+
}
|
|
1930
|
+
const layout = await getTmuxLayout(tmuxSessionName);
|
|
1931
|
+
return Promise.all(layout.windows.flatMap(window => window.panes).map(async (pane) => {
|
|
1932
|
+
const program = await resolveTmuxPaneProgram(pane, true, true);
|
|
1933
|
+
const agent = detectAgentFromCommand(program?.rawArgs ?? program?.command ?? '', agentCustomCommands());
|
|
1934
|
+
const shell = !agent && await integrationPaneIsIdleShell({ panePid: pane.pid,
|
|
1935
|
+
isShell: shellNamesBackend.has(normalizeProgramName(program?.command ?? '')?.toLowerCase() ?? '') });
|
|
1936
|
+
return { confirmed: shell || program?.source === 'tmux-tty' && Boolean(program.rawArgs),
|
|
1937
|
+
agentSlug: agent?.slug ?? null,
|
|
1938
|
+
nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null };
|
|
1939
|
+
}));
|
|
1940
|
+
}
|
|
1941
|
+
if (!backend)
|
|
1942
|
+
return [];
|
|
1943
|
+
const program = await detectShellActiveProgram(backend);
|
|
1944
|
+
const agent = detectAgentFromCommand(program?.rawArgs ?? program?.command ?? '', agentCustomCommands());
|
|
1945
|
+
return [{ confirmed: !!program && program.source !== 'unknown', agentSlug: agent?.slug ?? null,
|
|
1946
|
+
nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null }];
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1907
1949
|
async function launchIntegrationTerminal(record, restoring, prepared) {
|
|
1908
1950
|
await collaborationDeliveryWorker.reconfigure(record.session_id, async () => {
|
|
1909
1951
|
const group = collaborationStore.getGroup(record.group_id);
|
|
@@ -3826,9 +3868,9 @@ function parseProcessSnapshot(stdout) {
|
|
|
3826
3868
|
})
|
|
3827
3869
|
.filter((row) => row !== null);
|
|
3828
3870
|
}
|
|
3829
|
-
async function getProcessSnapshot() {
|
|
3871
|
+
async function getProcessSnapshot(refresh = false) {
|
|
3830
3872
|
const now = Date.now();
|
|
3831
|
-
if (processSnapshot && now - processSnapshot.fetchedAt < PROCESS_SNAPSHOT_CACHE_TTL_MS) {
|
|
3873
|
+
if (!refresh && processSnapshot && now - processSnapshot.fetchedAt < PROCESS_SNAPSHOT_CACHE_TTL_MS) {
|
|
3832
3874
|
return processSnapshot.rows;
|
|
3833
3875
|
}
|
|
3834
3876
|
if (processSnapshotPromise)
|
|
@@ -3846,7 +3888,7 @@ async function getProcessSnapshot() {
|
|
|
3846
3888
|
});
|
|
3847
3889
|
return processSnapshotPromise;
|
|
3848
3890
|
}
|
|
3849
|
-
async function resolveTmuxPaneProgram(pane, requireArgs = false) {
|
|
3891
|
+
async function resolveTmuxPaneProgram(pane, requireArgs = false, refreshTty = false) {
|
|
3850
3892
|
// If pane command is a known shell, try to find a child foreground process
|
|
3851
3893
|
const command = normalizeProgramName(pane.command);
|
|
3852
3894
|
const commandKey = command?.toLowerCase() ?? null;
|
|
@@ -3891,7 +3933,7 @@ async function resolveTmuxPaneProgram(pane, requireArgs = false) {
|
|
|
3891
3933
|
};
|
|
3892
3934
|
})
|
|
3893
3935
|
.filter((row) => row !== null);
|
|
3894
|
-
});
|
|
3936
|
+
}, refreshTty);
|
|
3895
3937
|
}
|
|
3896
3938
|
const selected = selectTmuxForegroundProgram({
|
|
3897
3939
|
panePid: pane.pid,
|
package/docs/integrations.md
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
最低 CLI / 运行服务版本:**termdock 1.4.300**,两者都必须升级并重启服务。
|
|
4
4
|
启动输入条件与保留凭据的策略更新要求 **CLI / 实际运行服务均至少 1.4.301**;协议仍为 1。
|
|
5
5
|
插件原生 ID 的前台 argv 回读及服务重启后的运行中 restore 核验修复要求
|
|
6
|
-
**实际运行服务至少 1.4.302
|
|
6
|
+
**实际运行服务至少 1.4.302**。退出后的重复原生会话检查修复要求
|
|
7
|
+
**实际运行服务至少 1.4.303**。没有 PTY 附着的已登记 tmux 的重复保护修复要求
|
|
8
|
+
**实际运行服务至少 1.4.304**;建议 CLI 与服务同步安装 1.4.304(CLI 最低仍为 1.4.301)。
|
|
7
9
|
现有 `--session`、消息投递、任务报告与自动协作入口继续兼容;旧的
|
|
8
10
|
`task create --integration` 仍表示代码集成子任务,新身份使用 `--principal`。
|
|
9
11
|
|
|
@@ -90,6 +92,9 @@ CLI 使用当前运行服务的端口,默认 9834。每个端口的身份、
|
|
|
90
92
|
|
|
91
93
|
TraeX 的恢复画面可能仅在 footer 显示模型,没有 `model:` 标题;因此示例不要求
|
|
92
94
|
该前缀。管理员须同时核对冷启动和恢复画面,条件不匹配时保持待投递。
|
|
95
|
+
模型 footer 可能被 Git 错误或键盘提示替换,此时含模型文字的 `allOf` 条件不会
|
|
96
|
+
匹配。请选择实际画面中稳定且能区分加载阶段的文字;TD 不用原生 UUID ready、
|
|
97
|
+
历史模型文字或推测的忙闲状态代替配置条件,超时也不会自动放宽。
|
|
93
98
|
|
|
94
99
|
`allOf` 的每个字面文本都须出现,`noneOf` 的每个文本都须缺席;区分大小写,
|
|
95
100
|
连续横向空白折叠为一个空格,不使用正则或终端历史。每组最多 16 项,每项
|
|
@@ -292,11 +297,24 @@ session 事件;get 也会现场核验。120 秒内未形成绑定返回 `SESSI
|
|
|
292
297
|
服务重启后,原 pane 中精确 UUID 已在运行时 get 应回读 ready,restore 返回原
|
|
293
298
|
operation/pane,不启动第二个 Agent;进程携带不同 UUID 时继续拒绝。
|
|
294
299
|
|
|
300
|
+
1.4.303 起,集成 restore 排除自己的已核验终端;其他已登记终端须用当前前台
|
|
301
|
+
进程证明同一个 UUID 才返回 `NATIVE_SESSION_ALREADY_RUNNING`。已退出到 shell、
|
|
302
|
+
已消失或实际运行另一 UUID 的终端,其缓存/last-known 记录不算运行中所有者。
|
|
303
|
+
可能持有原 UUID 的终端无法核验、或相同 Agent 正在运行但没有精确 ID 证据时,
|
|
304
|
+
返回 `NATIVE_SESSION_OWNER_UNCONFIRMED` 并停止恢复;应检查该终端,不绕过保护。
|
|
305
|
+
此检查覆盖服务已登记终端,不声称枚举其他应用中全部原生会话。
|
|
306
|
+
|
|
307
|
+
1.4.304 起,检查合并持久终端清单和当前 PTY backend;服务重启后无需网页挂接,
|
|
308
|
+
已登记 tmux 的每个 pane 都会现场核验。旧登记 UUID C 不会排除当前实际运行 UUID A
|
|
309
|
+
的终端,终端的协作路由为 recovering/offline 或绑定不匹配也不会跳过检查。
|
|
310
|
+
仅在确认 tmux 已消失或当前进程排除冲突时继续;未知进程、同 Agent 缺少精确 UUID、
|
|
311
|
+
观察失败均返回 `NATIVE_SESSION_OWNER_UNCONFIRMED`,不以旧 UUID 证明没有冲突。
|
|
312
|
+
|
|
295
313
|
常见失败码(每项都是完整字符串):
|
|
296
314
|
|
|
297
315
|
- 配置/权限:`LAUNCH_PROFILE_DENIED`, `LAUNCHER_UNAVAILABLE`, `COLLAB_AGENT_UNAVAILABLE`, `SESSION_CWD_MISSING`, `SESSION_CWD_DENIED`, `INTEGRATION_SESSION_NOT_FOUND`, `INTEGRATION_SESSION_LIMIT`。
|
|
298
316
|
- 终端/身份:`SESSION_BACKEND_UNAVAILABLE`, `SESSION_TARGET_NOT_SHELL`, `SESSION_IDENTITY_MISMATCH`, `NATIVE_SESSION_ID_MISMATCH`, `NATIVE_SESSION_ID_INVALID`, `NATIVE_SESSION_ID_MISSING`。
|
|
299
|
-
- 启动/恢复:`SESSION_CREATE_OUTCOME_UNKNOWN`, `SESSION_OPERATION_IN_PROGRESS`, `SESSION_BINDING_TIMEOUT`, `EXACT_RESUME_UNSUPPORTED`, `NATIVE_SESSION_ALREADY_RUNNING`, `AGENT_NOT_RUNNING`。
|
|
317
|
+
- 启动/恢复:`SESSION_CREATE_OUTCOME_UNKNOWN`, `SESSION_OPERATION_IN_PROGRESS`, `SESSION_BINDING_TIMEOUT`, `EXACT_RESUME_UNSUPPORTED`, `NATIVE_SESSION_ALREADY_RUNNING`, `NATIVE_SESSION_OWNER_UNCONFIRMED`, `AGENT_NOT_RUNNING`。
|
|
300
318
|
- 未分类异常:`SESSION_LAUNCH_FAILED`, `SESSION_OBSERVATION_FAILED`;错误不包含包装器输出、密钥或原始堆栈。
|
|
301
319
|
|
|
302
320
|
## 验收边界
|
|
@@ -311,8 +329,13 @@ operation/pane,不启动第二个 Agent;进程携带不同 UUID 时继续拒
|
|
|
311
329
|
原 UUID、TD ID、attempt 和 artifact 保持一致。使用方已报告 1.4.301 冷启动首条
|
|
312
330
|
仅写一次并收到明确 complete,超时保持 pending/0 次写入,策略更新保持凭据。
|
|
313
331
|
维护机另外用真实 tmux/native fixture 验证插件 argv 回读、记录重载后运行中恢复
|
|
314
|
-
不重复启动、不同 UUID
|
|
315
|
-
|
|
332
|
+
不重复启动、不同 UUID 拒绝,以及进程退出后缓存 UUID 不再阻挡恢复。使用方已报告
|
|
333
|
+
1.4.302 真实 TraeX 服务重启回读、运行中恢复不重启、不同 UUID 拒绝通过;显式恢复
|
|
334
|
+
原 UUID 也已回读 ready,但配置的模型 footer 条件被暂态提示替换,输入仍保持待投递。
|
|
335
|
+
使用方已报告 1.4.303 退出后精确恢复通过,也发现未挂接 PTY 的已登记 tmux 重复保护
|
|
336
|
+
遗漏;维护机已用“旧 UUID C、实际运行 UUID A、无 PTY 附着”的真实隔离 tmux 复现,
|
|
337
|
+
并验证 1.4.304 编译后检查在该状态下拒绝再次启动。
|
|
338
|
+
**1.4.304 的真实 TraeX 重复保护与稳定输入条件仍须使用方实机复验**。
|
|
316
339
|
回读 capabilities 后,应核对原 TD ID、真实 TraeX UUID 与原 argv/cwd/包装器快照,
|
|
317
340
|
并验证 Agent 真正加载原对话。
|
|
318
341
|
先完成测试环境的创建→追问→关联答复,再验重复键、追加咨询、断线重投和精确恢复。
|
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.304",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
8
|
"dependencyHash": "sha256-E9GDa4nrCXADRXdyPKufusZt6IcCr4m2mGdx5h34KMQ=",
|
|
9
|
-
"serverBundleHash": "sha256-
|
|
9
|
+
"serverBundleHash": "sha256-7LaEwk0ur5djmnXCjBBkhMPg2z5dB5dcYm7tmh14Pko=",
|
|
10
10
|
"clientBundleHash": "sha256-HOoNMOYhnph9XwUhSYE5W2CdI9Vb4g0c64ubW49LnXE=",
|
|
11
11
|
"entrypoint": "dist/server/cli.js",
|
|
12
12
|
"clientEntrypoint": "dist/client/index.html"
|