termdock 1.4.304 → 1.4.305
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/integrationCli.js +11 -1
- package/dist/server/agent/integrationServer.js +2 -0
- package/dist/server/agent/integrationSessions.js +15 -3
- package/dist/server/agent/nativeResumeOwner.js +39 -9
- package/dist/server/routes/terminal.js +22 -11
- package/docs/integrations.md +53 -3
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
|
@@ -8,6 +8,7 @@ import { executeCollaborationCommand, duration } from './collaborationCli.js';
|
|
|
8
8
|
import { getTermdockVersion } from '../utils/version.js';
|
|
9
9
|
export const INTEGRATION_HELP = `td integration — local scoped integration administration
|
|
10
10
|
groups | list
|
|
11
|
+
diagnostics <TD-session-id> (read the last restore check; administrator only)
|
|
11
12
|
create --file <policy.json> --credential-file <private-output.json>
|
|
12
13
|
update --file <policy.json> (same identity/group; preserves the credential)
|
|
13
14
|
revoke <principal-id>
|
|
@@ -58,13 +59,22 @@ export async function runIntegrationAdmin(argv, port, adminToken, write) {
|
|
|
58
59
|
write(INTEGRATION_HELP);
|
|
59
60
|
return 0;
|
|
60
61
|
}
|
|
62
|
+
const socketPath = integrationSocketPath(port);
|
|
63
|
+
if (action === 'diagnostics') {
|
|
64
|
+
if (argv.length !== 2 || !/^[a-zA-Z0-9._-]{1,80}$/.test(argv[1]))
|
|
65
|
+
throw new Error('diagnostics requires one TD session id');
|
|
66
|
+
const capabilities = parsed(await request(socketPath, 'GET', '/admin/capabilities', undefined, headers(undefined, adminToken)));
|
|
67
|
+
if (capabilities.session_restore_diagnostics !== true)
|
|
68
|
+
throw new CollaborationError('SESSION_RESTORE_DIAGNOSTICS_UNSUPPORTED', 'Running service does not support stored restore diagnostics', 409);
|
|
69
|
+
write(JSON.stringify(parsed(await request(socketPath, 'GET', `/admin/sessions/${encodeURIComponent(argv[1])}/restore-diagnostics`, undefined, headers(undefined, adminToken)))));
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
61
72
|
for (let index = 1; index < argv.length; index += 2) {
|
|
62
73
|
const flag = argv[index];
|
|
63
74
|
if (!['--file', '--credential-file'].includes(flag) || !argv[index + 1] || options[flag])
|
|
64
75
|
throw new Error('Invalid integration arguments');
|
|
65
76
|
options[flag] = argv[index + 1];
|
|
66
77
|
}
|
|
67
|
-
const socketPath = integrationSocketPath(port);
|
|
68
78
|
if (action === 'groups' || action === 'list') {
|
|
69
79
|
if (argv.length !== 1)
|
|
70
80
|
throw new Error('Unexpected arguments');
|
|
@@ -61,6 +61,7 @@ export class IntegrationRuntime {
|
|
|
61
61
|
};
|
|
62
62
|
app.get('/admin/groups', run(req => { admin(req); return { groups: options.messages.list().filter(g => !g.federated).map(g => ({ id: g.id, name: g.name })) }; }));
|
|
63
63
|
app.get('/admin/capabilities', run(req => { admin(req); return this.capabilities(); }));
|
|
64
|
+
app.get('/admin/sessions/:id/restore-diagnostics', run(req => { admin(req); return this.sessions.restoreDiagnostics(req.params.id); }));
|
|
64
65
|
app.get('/admin/principals', run(req => { admin(req); return { integrations: this.store.policies() }; }));
|
|
65
66
|
app.post('/admin/principals', run(req => { admin(req); this.group(req.body?.groupId); const provisioned = this.store.provision(req.body); try {
|
|
66
67
|
this.sync();
|
|
@@ -145,6 +146,7 @@ export class IntegrationRuntime {
|
|
|
145
146
|
idempotent_session_create: true, exact_session_restore: true, external_validation: false,
|
|
146
147
|
startup_input_conditions: true,
|
|
147
148
|
integration_policy_update: true,
|
|
149
|
+
session_restore_diagnostics: true,
|
|
148
150
|
permissions: ['task.read', 'task.create', 'task.assign', 'task.comment', 'task.revise', 'task.answer', 'events.read', 'session.create', 'session.read', 'session.restore'] };
|
|
149
151
|
}
|
|
150
152
|
error(error) {
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
4
4
|
import { atomicJson, integrationError } from './integrationStore.js';
|
|
5
|
+
import { NativeResumeOwnerError } from './nativeResumeOwner.js';
|
|
5
6
|
/** Quote each configured argument as literal shell data, including paths. */
|
|
6
7
|
export function integrationLaunchCommand(record, restore) {
|
|
7
8
|
const launchArgs = record.profile.argv.map(arg => arg === '{cwd}' ? record.cwd : arg);
|
|
@@ -15,8 +16,8 @@ function startupInput(record, now) {
|
|
|
15
16
|
return condition ? { state: 'pending', deadline: now + (condition.timeoutMs ?? 120000), matched_since: null, observed_at: null } : undefined;
|
|
16
17
|
}
|
|
17
18
|
export function publicIntegrationSession(record) {
|
|
18
|
-
const { profile: _, deadline: _deadline, launch_submitted: _submitted, startup_condition: _condition, ...publicRecord } = record;
|
|
19
|
-
return publicRecord;
|
|
19
|
+
const { profile: _, deadline: _deadline, launch_submitted: _submitted, startup_condition: _condition, restore_diagnostics: diagnostics, ...publicRecord } = record;
|
|
20
|
+
return { ...publicRecord, ...(diagnostics ? { restore_diagnostics_available: true } : {}) };
|
|
20
21
|
}
|
|
21
22
|
export function permittedCwd(profile, cwd) {
|
|
22
23
|
if (typeof cwd !== 'string' || !path.isAbsolute(cwd) || /[\x00-\x1f\x7f]/.test(cwd))
|
|
@@ -168,7 +169,9 @@ export class IntegrationSessions {
|
|
|
168
169
|
catch (error) {
|
|
169
170
|
const current = this.doc.sessions.find(s => s.session_id === record.session_id);
|
|
170
171
|
const code = error.code;
|
|
171
|
-
this.saveRecord({ ...current, state: 'failed', error_code: typeof code === 'string' ? code : 'SESSION_LAUNCH_FAILED',
|
|
172
|
+
this.saveRecord({ ...current, state: 'failed', error_code: typeof code === 'string' ? code : 'SESSION_LAUNCH_FAILED',
|
|
173
|
+
restore_diagnostics: error instanceof NativeResumeOwnerError ? { ...structuredClone(error.diagnostics), operation_id: current.operation_id } : undefined,
|
|
174
|
+
updated_at: Date.now() });
|
|
172
175
|
}
|
|
173
176
|
return structuredClone(this.doc.sessions.find(s => s.session_id === record.session_id));
|
|
174
177
|
})().finally(() => this.pending.delete(record.session_id));
|
|
@@ -176,6 +179,14 @@ export class IntegrationSessions {
|
|
|
176
179
|
return promise;
|
|
177
180
|
}
|
|
178
181
|
async get(principal, id) { this.lookup(principal, id); await this.refresh(id); return this.lookup(principal, id); }
|
|
182
|
+
/** Read the stored check only: no process probe, PTY attachment, or new launch. */
|
|
183
|
+
restoreDiagnostics(id) {
|
|
184
|
+
const record = this.doc.sessions.find(session => session.session_id === id);
|
|
185
|
+
if (!record)
|
|
186
|
+
return integrationError('INTEGRATION_SESSION_NOT_FOUND', 'Integration session does not exist', 404);
|
|
187
|
+
return structuredClone({ session_id: record.session_id, operation_id: record.operation_id,
|
|
188
|
+
state: record.state, error_code: record.error_code, diagnostics: record.restore_diagnostics ?? null });
|
|
189
|
+
}
|
|
179
190
|
async restore(principal, id, idempotencyKey) {
|
|
180
191
|
const request = this.request(principal, idempotencyKey, ['restore', id]);
|
|
181
192
|
if (request.previous)
|
|
@@ -215,6 +226,7 @@ export class IntegrationSessions {
|
|
|
215
226
|
record.operation_id = randomUUID();
|
|
216
227
|
record.deadline = Date.now() + this.bindingTimeout;
|
|
217
228
|
record.updated_at = Date.now();
|
|
229
|
+
record.restore_diagnostics = undefined;
|
|
218
230
|
if (!record.profile.startupInput && !record.startup_condition)
|
|
219
231
|
record.startup_condition = structuredClone(principal.launchProfiles.find(profile => profile.id === record.launch_profile)?.startupInput);
|
|
220
232
|
record.startup_input = startupInput(record, Date.now());
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CollaborationError } from './collaborationProtocol.js';
|
|
2
2
|
/** A tmux remains observable without a browser/PTY attachment, including after
|
|
3
3
|
* service restart. Inventory UUIDs never filter the candidate set. */
|
|
4
4
|
export function collectNativeResumeOwnerCandidates(inventory, backends) {
|
|
@@ -9,6 +9,7 @@ export function collectNativeResumeOwnerCandidates(inventory, backends) {
|
|
|
9
9
|
continue;
|
|
10
10
|
const backend = entry.backendSessionId ? backends.get(entry.backendSessionId) : null;
|
|
11
11
|
candidates.push({ backendSessionId: entry.backendSessionId ?? `inventory:${entry.sessionId}`,
|
|
12
|
+
sessionId: entry.sessionId, backendAttached: !!backend,
|
|
12
13
|
tmuxSessionName: entry.tmuxSessionName, cachedSlug: backend?.agent?.slug ?? entry.agentResume?.slug ?? null,
|
|
13
14
|
cachedNativeId: backend?.agentSession?.sessionId ?? entry.agentResume?.sessionId ?? null });
|
|
14
15
|
seenTmux.add(entry.tmuxSessionName);
|
|
@@ -18,17 +19,42 @@ export function collectNativeResumeOwnerCandidates(inventory, backends) {
|
|
|
18
19
|
for (const [backendSessionId, backend] of backends) {
|
|
19
20
|
if (seenBackend.has(backendSessionId) || backend.mode === 'tmux' && backend.tmuxSessionName && seenTmux.has(backend.tmuxSessionName))
|
|
20
21
|
continue;
|
|
21
|
-
candidates.push({ backendSessionId,
|
|
22
|
+
candidates.push({ backendSessionId, sessionId: inventory.find(entry => entry.backendSessionId === backendSessionId)?.sessionId ?? null,
|
|
23
|
+
backendAttached: true, tmuxSessionName: backend.mode === 'tmux' ? backend.tmuxSessionName : null,
|
|
22
24
|
cachedSlug: backend.agent?.slug ?? null, cachedNativeId: backend.agentSession?.sessionId ?? null });
|
|
23
25
|
if (backend.mode === 'tmux' && backend.tmuxSessionName)
|
|
24
26
|
seenTmux.add(backend.tmuxSessionName);
|
|
25
27
|
}
|
|
26
28
|
return candidates;
|
|
27
29
|
}
|
|
30
|
+
export class NativeResumeOwnerError extends CollaborationError {
|
|
31
|
+
diagnostics;
|
|
32
|
+
constructor(code, blockers) {
|
|
33
|
+
super(code, code === 'NATIVE_SESSION_ALREADY_RUNNING'
|
|
34
|
+
? 'Another terminal process proves the exact native conversation is running'
|
|
35
|
+
: 'A possible native owner cannot be verified; inspect its terminal before restoring', 409);
|
|
36
|
+
// Preserve the proven owner even when earlier uncertain panes exceed the bound.
|
|
37
|
+
const ordered = code === 'NATIVE_SESSION_ALREADY_RUNNING' ? [blockers.at(-1), ...blockers.slice(0, -1)] : blockers;
|
|
38
|
+
this.diagnostics = { code, checked_at: Date.now(), candidates: ordered.slice(0, 128), total_blockers: blockers.length, truncated: blockers.length > 128 };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function diagnostic(candidate, process, reason) {
|
|
42
|
+
// Store only named locator/evidence fields. Never serialize a callback error,
|
|
43
|
+
// argv, terminal text, cwd, or plugin/wrapper payload into diagnostics.
|
|
44
|
+
const text = (value) => typeof value === 'string' && value.length <= 256 && !/[\x00-\x1f\x7f]/.test(value) ? value : null;
|
|
45
|
+
const pid = (value) => Number.isInteger(value) && value > 0 ? value : null;
|
|
46
|
+
return { session_id: text(candidate.sessionId), backend_session_id: candidate.backendSessionId.startsWith('inventory:') ? null : text(candidate.backendSessionId),
|
|
47
|
+
backend_attached: candidate.backendAttached ?? null, tmux_session_name: text(candidate.tmuxSessionName),
|
|
48
|
+
tmux_session_id: text(process?.tmuxSessionId), pane_id: text(process?.paneId), pane_pid: pid(process?.panePid),
|
|
49
|
+
program: text(process?.program), process_source: text(process?.processSource), arguments_observed: process?.argumentsObserved === true,
|
|
50
|
+
agent_slug: text(process?.agentSlug), last_known_native_id: text(candidate.cachedNativeId), observed_native_id: text(process?.nativeId),
|
|
51
|
+
pgid: Number.isInteger(process?.pgid) ? process.pgid : null,
|
|
52
|
+
tpgid: Number.isInteger(process?.tpgid) ? process.tpgid : null, reason, observed_at: Date.now() };
|
|
53
|
+
}
|
|
28
54
|
/** Only process observations prove presence or absence of another owner.
|
|
29
55
|
* Cached UUIDs cannot exclude a terminal; the caller verifies its own pane. */
|
|
30
56
|
export async function assertNativeResumeAvailable(target, excludedBackendSessionId, candidates, observe) {
|
|
31
|
-
|
|
57
|
+
const blockers = [];
|
|
32
58
|
for (const candidate of candidates) {
|
|
33
59
|
if (candidate.backendSessionId === excludedBackendSessionId)
|
|
34
60
|
continue;
|
|
@@ -37,17 +63,21 @@ export async function assertNativeResumeAvailable(target, excludedBackendSession
|
|
|
37
63
|
processes = await observe(candidate);
|
|
38
64
|
}
|
|
39
65
|
catch {
|
|
40
|
-
|
|
66
|
+
blockers.push(diagnostic(candidate, null, 'TERMINAL_OBSERVATION_FAILED'));
|
|
41
67
|
continue;
|
|
42
68
|
}
|
|
43
69
|
for (const process of processes) {
|
|
44
70
|
if (process.confirmed && process.agentSlug === target.slug && process.nativeId === target.nativeSessionId) {
|
|
45
|
-
|
|
71
|
+
blockers.push(diagnostic(candidate, process, 'NATIVE_SESSION_MATCH'));
|
|
72
|
+
throw new NativeResumeOwnerError('NATIVE_SESSION_ALREADY_RUNNING', blockers);
|
|
46
73
|
}
|
|
47
|
-
if (!process.confirmed
|
|
48
|
-
|
|
74
|
+
if (!process.confirmed)
|
|
75
|
+
blockers.push(diagnostic(candidate, process, process.shellFailure
|
|
76
|
+
?? (process.argumentsObserved === false ? 'PROCESS_ARGUMENTS_UNAVAILABLE' : 'FOREGROUND_PROCESS_UNCONFIRMED')));
|
|
77
|
+
else if (process.agentSlug === target.slug && !process.nativeId)
|
|
78
|
+
blockers.push(diagnostic(candidate, process, 'NATIVE_SESSION_ID_MISSING'));
|
|
49
79
|
}
|
|
50
80
|
}
|
|
51
|
-
if (
|
|
52
|
-
|
|
81
|
+
if (blockers.length)
|
|
82
|
+
throw new NativeResumeOwnerError('NATIVE_SESSION_OWNER_UNCONFIRMED', blockers);
|
|
53
83
|
}
|
|
@@ -1825,19 +1825,26 @@ async function spawnCollaborationAgentSession(req, group, sourceSessionId, input
|
|
|
1825
1825
|
/** Restricted session adapter: only preconfigured argv and the exact owned
|
|
1826
1826
|
* terminal can be launched; no remote drive or arbitrary shell commands. */
|
|
1827
1827
|
async function integrationPaneIsIdleShell(pane) {
|
|
1828
|
+
return (await inspectIntegrationShell(pane)).confirmed;
|
|
1829
|
+
}
|
|
1830
|
+
async function inspectIntegrationShell(pane) {
|
|
1828
1831
|
if (!pane.isShell)
|
|
1829
|
-
return false;
|
|
1832
|
+
return { confirmed: false, failure: null, pgid: null, tpgid: null };
|
|
1830
1833
|
try {
|
|
1831
1834
|
const { stdout } = await execFileAsync('ps', ['-p', String(pane.panePid), '-o', 'pgid=,tpgid=,args='], { timeout: 3000, maxBuffer: 16384 });
|
|
1832
|
-
const row = stdout.trim().match(/^(
|
|
1833
|
-
if (!row
|
|
1834
|
-
return false;
|
|
1835
|
+
const row = stdout.trim().match(/^(-?\d+)\s+(-?\d+)\s+(.+)$/);
|
|
1836
|
+
if (!row)
|
|
1837
|
+
return { confirmed: false, failure: 'SHELL_PROCESS_NOT_FOUND', pgid: null, tpgid: null };
|
|
1838
|
+
const pgid = Number(row[1]), tpgid = Number(row[2]);
|
|
1839
|
+
if (pgid <= 0 || pgid !== tpgid)
|
|
1840
|
+
return { confirmed: false, failure: 'SHELL_FOREGROUND_MISMATCH', pgid, tpgid };
|
|
1835
1841
|
const argv = splitCommandToArgv(row[3]);
|
|
1836
1842
|
const command = normalizeProgramName(argv[0]?.replace(/^-/, ''))?.toLowerCase();
|
|
1837
|
-
|
|
1843
|
+
const confirmed = !!command && shellNamesBackend.has(command) && argv.slice(1).every(arg => ['-l', '-i', '-il', '-li', '--login', '--interactive'].includes(arg));
|
|
1844
|
+
return { confirmed, failure: confirmed ? null : 'SHELL_ARGUMENTS_UNSUPPORTED', pgid, tpgid };
|
|
1838
1845
|
}
|
|
1839
1846
|
catch {
|
|
1840
|
-
return false;
|
|
1847
|
+
return { confirmed: false, failure: 'SHELL_OBSERVATION_FAILED', pgid: null, tpgid: null };
|
|
1841
1848
|
}
|
|
1842
1849
|
}
|
|
1843
1850
|
function integrationTerminalRecord(record) {
|
|
@@ -1931,11 +1938,14 @@ async function assertIntegrationNativeResumeAvailable(record) {
|
|
|
1931
1938
|
return Promise.all(layout.windows.flatMap(window => window.panes).map(async (pane) => {
|
|
1932
1939
|
const program = await resolveTmuxPaneProgram(pane, true, true);
|
|
1933
1940
|
const agent = detectAgentFromCommand(program?.rawArgs ?? program?.command ?? '', agentCustomCommands());
|
|
1934
|
-
const shell =
|
|
1935
|
-
isShell: shellNamesBackend.has(normalizeProgramName(program?.command ?? '')?.toLowerCase() ?? '') });
|
|
1936
|
-
return { confirmed: shell || program?.source === 'tmux-tty' && Boolean(program.rawArgs),
|
|
1941
|
+
const shell = await inspectIntegrationShell({ panePid: pane.pid,
|
|
1942
|
+
isShell: !agent && shellNamesBackend.has(normalizeProgramName(program?.command ?? '')?.toLowerCase() ?? '') });
|
|
1943
|
+
return { confirmed: shell.confirmed || program?.source === 'tmux-tty' && Boolean(program.rawArgs),
|
|
1937
1944
|
agentSlug: agent?.slug ?? null,
|
|
1938
|
-
nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null
|
|
1945
|
+
nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null,
|
|
1946
|
+
paneId: pane.id, panePid: pane.pid, tmuxSessionId: layout.sessionId, program: program?.command ?? null,
|
|
1947
|
+
processSource: program?.source, argumentsObserved: !!program?.rawArgs,
|
|
1948
|
+
shellFailure: shell.failure, pgid: shell.pgid, tpgid: shell.tpgid };
|
|
1939
1949
|
}));
|
|
1940
1950
|
}
|
|
1941
1951
|
if (!backend)
|
|
@@ -1943,7 +1953,8 @@ async function assertIntegrationNativeResumeAvailable(record) {
|
|
|
1943
1953
|
const program = await detectShellActiveProgram(backend);
|
|
1944
1954
|
const agent = detectAgentFromCommand(program?.rawArgs ?? program?.command ?? '', agentCustomCommands());
|
|
1945
1955
|
return [{ confirmed: !!program && program.source !== 'unknown', agentSlug: agent?.slug ?? null,
|
|
1946
|
-
nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null
|
|
1956
|
+
nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null,
|
|
1957
|
+
program: program?.command ?? null, processSource: program?.source, argumentsObserved: !!program?.rawArgs }];
|
|
1947
1958
|
});
|
|
1948
1959
|
}
|
|
1949
1960
|
async function launchIntegrationTerminal(record, restoring, prepared) {
|
package/docs/integrations.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
插件原生 ID 的前台 argv 回读及服务重启后的运行中 restore 核验修复要求
|
|
6
6
|
**实际运行服务至少 1.4.302**。退出后的重复原生会话检查修复要求
|
|
7
7
|
**实际运行服务至少 1.4.303**。没有 PTY 附着的已登记 tmux 的重复保护修复要求
|
|
8
|
-
**实际运行服务至少 1.4.304
|
|
8
|
+
**实际运行服务至少 1.4.304**。管理员恢复诊断要求 **CLI / 实际服务均至少 1.4.305**;
|
|
9
|
+
建议 CLI 与服务同步安装 1.4.305(既有接入命令的 CLI 最低仍为 1.4.301)。
|
|
9
10
|
现有 `--session`、消息投递、任务报告与自动协作入口继续兼容;旧的
|
|
10
11
|
`task create --integration` 仍表示代码集成子任务,新身份使用 `--principal`。
|
|
11
12
|
|
|
@@ -310,9 +311,55 @@ operation/pane,不启动第二个 Agent;进程携带不同 UUID 时继续拒
|
|
|
310
311
|
仅在确认 tmux 已消失或当前进程排除冲突时继续;未知进程、同 Agent 缺少精确 UUID、
|
|
311
312
|
观察失败均返回 `NATIVE_SESSION_OWNER_UNCONFIRMED`,不以旧 UUID 证明没有冲突。
|
|
312
313
|
|
|
314
|
+
### 管理员恢复诊断(1.4.305)
|
|
315
|
+
|
|
316
|
+
`capabilities.session_restore_diagnostics=true` 表示服务支持持久恢复诊断。
|
|
317
|
+
遇到 `NATIVE_SESSION_OWNER_UNCONFIRMED` 或 `NATIVE_SESSION_ALREADY_RUNNING` 后,
|
|
318
|
+
本机管理员执行:
|
|
319
|
+
|
|
320
|
+
```sh
|
|
321
|
+
td integration diagnostics <TD-session-id>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
命令经原私有 Unix socket 和本机管理员凭据读取报告,不使用 `--principal`。
|
|
325
|
+
服务能力缺失时返回 `SESSION_RESTORE_DIAGNOSTICS_UNSUPPORTED`,不会假装诊断成功。
|
|
326
|
+
命令仅读取已存记录,不重新扫描进程、不挂接 PTY、不试键、不创建恢复意图。
|
|
327
|
+
304 及更早的失败没有此报告;升级后应使用新的 restore 幂等键执行一次明确检查,
|
|
328
|
+
再读报告。旧键不会执行一次新检查。
|
|
329
|
+
|
|
330
|
+
响应包含当前 `session_id / operation_id / state / error_code`,以及可为空的
|
|
331
|
+
`diagnostics`。报告含其自身 `operation_id / checked_at / code`、`candidates`、
|
|
332
|
+
`total_blockers / truncated`;它是该次检查的历史事实,不能代表读取时的当前进程。
|
|
333
|
+
只有上一次**实际发起**的恢复检查会保存报告;下一次实际恢复清除旧报告,运行中
|
|
334
|
+
no-op restore 不创建新检查。报告跟随会话保存并可在服务重启后回读。
|
|
335
|
+
|
|
336
|
+
每个阻塞候选含 `session_id / backend_session_id / backend_attached`、
|
|
337
|
+
`tmux_session_name / tmux_session_id / pane_id / pane_pid`、`program / process_source`、
|
|
338
|
+
`arguments_observed / agent_slug / last_known_native_id / observed_native_id`、
|
|
339
|
+
`pgid / tpgid / reason / observed_at`;无法观测的值为 null。backend 不存在或仅有旧
|
|
340
|
+
登记仍可通过 tmux 核验;`pane_pid` 是承载 pane 的 PID,不冒充 Agent 前台 PID。
|
|
341
|
+
报告最多保存 128 个阻塞候选;`total_blockers` 保留总数,确定的同 UUID 所有者优先展示。
|
|
342
|
+
|
|
343
|
+
| reason | 实际证据缺口或冲突 |
|
|
344
|
+
| --- | --- |
|
|
345
|
+
| NATIVE_SESSION_MATCH | 当前前台进程明确携带目标 UUID |
|
|
346
|
+
| NATIVE_SESSION_ID_MISSING | 当前同 Agent 的进程已观测,但没有可回读的精确 UUID |
|
|
347
|
+
| PROCESS_ARGUMENTS_UNAVAILABLE | 无法取得可靠的当前进程 argv |
|
|
348
|
+
| FOREGROUND_PROCESS_UNCONFIRMED | 无法可靠核验前台进程 |
|
|
349
|
+
| SHELL_PROCESS_NOT_FOUND | shell 的 ps 输出未形成可解析记录 |
|
|
350
|
+
| SHELL_FOREGROUND_MISMATCH | shell 未持有有效前台进程组;记录 pgid/tpgid |
|
|
351
|
+
| SHELL_ARGUMENTS_UNSUPPORTED | 当前 shell 命令或启动参数未满足既有安全检查 |
|
|
352
|
+
| SHELL_OBSERVATION_FAILED | shell 的进程观察失败 |
|
|
353
|
+
| TERMINAL_OBSERVATION_FAILED | tmux 或终端观察失败,无法排除该候选 |
|
|
354
|
+
|
|
355
|
+
完整候选可能来自其他组,因此仅管理员接口返回;principal 的 session 响应与推送
|
|
356
|
+
事件只增加 `restore_diagnostics_available=true`,不含候选标识/UUID。报告不保存或
|
|
357
|
+
返回原始 argv、cwd、终端正文、包装器异常、凭据。诊断不自动放宽任何拒绝条件,
|
|
358
|
+
不能把旧 UUID、native ready 或 Agent 忙闲推测当作消除冲突的证据。
|
|
359
|
+
|
|
313
360
|
常见失败码(每项都是完整字符串):
|
|
314
361
|
|
|
315
|
-
- 配置/权限:`LAUNCH_PROFILE_DENIED`, `LAUNCHER_UNAVAILABLE`, `COLLAB_AGENT_UNAVAILABLE`, `SESSION_CWD_MISSING`, `SESSION_CWD_DENIED`, `INTEGRATION_SESSION_NOT_FOUND`, `INTEGRATION_SESSION_LIMIT`。
|
|
362
|
+
- 配置/权限:`LAUNCH_PROFILE_DENIED`, `LAUNCHER_UNAVAILABLE`, `COLLAB_AGENT_UNAVAILABLE`, `SESSION_CWD_MISSING`, `SESSION_CWD_DENIED`, `INTEGRATION_SESSION_NOT_FOUND`, `INTEGRATION_SESSION_LIMIT`, `SESSION_RESTORE_DIAGNOSTICS_UNSUPPORTED`。
|
|
316
363
|
- 终端/身份:`SESSION_BACKEND_UNAVAILABLE`, `SESSION_TARGET_NOT_SHELL`, `SESSION_IDENTITY_MISMATCH`, `NATIVE_SESSION_ID_MISMATCH`, `NATIVE_SESSION_ID_INVALID`, `NATIVE_SESSION_ID_MISSING`。
|
|
317
364
|
- 启动/恢复:`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`。
|
|
318
365
|
- 未分类异常:`SESSION_LAUNCH_FAILED`, `SESSION_OBSERVATION_FAILED`;错误不包含包装器输出、密钥或原始堆栈。
|
|
@@ -335,7 +382,10 @@ operation/pane,不启动第二个 Agent;进程携带不同 UUID 时继续拒
|
|
|
335
382
|
使用方已报告 1.4.303 退出后精确恢复通过,也发现未挂接 PTY 的已登记 tmux 重复保护
|
|
336
383
|
遗漏;维护机已用“旧 UUID C、实际运行 UUID A、无 PTY 附着”的真实隔离 tmux 复现,
|
|
337
384
|
并验证 1.4.304 编译后检查在该状态下拒绝再次启动。
|
|
338
|
-
|
|
385
|
+
使用方已报告 1.4.304 在未挂接网页/PTY、旧登记 UUID C 而实际运行 A 的场景下,
|
|
386
|
+
正确拒绝同 UUID 恢复且没有第二个进程。所有者退出后仍有未确认候选,不能仅凭
|
|
387
|
+
错误码判断是哪一个 pane;1.4.305 提供管理员持久诊断,并保持原拒绝条件。
|
|
388
|
+
**1.4.305 的真实 TraeX 阻塞候选诊断与稳定输入条件仍须使用方实机复验**。
|
|
339
389
|
回读 capabilities 后,应核对原 TD ID、真实 TraeX UUID 与原 argv/cwd/包装器快照,
|
|
340
390
|
并验证 Agent 真正加载原对话。
|
|
341
391
|
先完成测试环境的创建→追问→关联答复,再验重复键、追加咨询、断线重投和精确恢复。
|
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.305",
|
|
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-XkM6+bMn6xdVpuXe8oaPunofP3J+MgVfh7lwiOhSY4I=",
|
|
10
10
|
"clientBundleHash": "sha256-HOoNMOYhnph9XwUhSYE5W2CdI9Vb4g0c64ubW49LnXE=",
|
|
11
11
|
"entrypoint": "dist/server/cli.js",
|
|
12
12
|
"clientEntrypoint": "dist/client/index.html"
|