termdock 1.4.345 → 1.4.346
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/collaborationCli.js +3 -0
- package/dist/server/agent/integrationCli.js +16 -2
- package/dist/server/agent/integrationServer.js +4 -1
- package/dist/server/agent/integrationShellWait.js +33 -0
- package/dist/server/agent/integrationStore.js +4 -0
- package/dist/server/agent/nativeProcessIdentity.js +21 -0
- package/dist/server/agent/sessionLaunchDiagnostics.js +2 -0
- package/dist/server/agent/sessionLaunchState.js +35 -1
- package/dist/server/routes/terminal.js +70 -24
- package/docs/integrations.md +29 -0
- package/package.json +1 -1
- package/runtime-manifest.json +2 -2
|
@@ -69,6 +69,9 @@ export const COLLAB_HELP = `td collab — durable messages; no agent-specific ho
|
|
|
69
69
|
消息历史只覆盖仍保留记录;不存在的 after-id 返回 MESSAGE_NOT_FOUND,不能据此声称历史完整。
|
|
70
70
|
session create --group <id> --launch-profile <id> --cwd <absolute-path> --idempotency-key <key>
|
|
71
71
|
session get <id> | session restore <id> --idempotency-key <key>
|
|
72
|
+
启动一致性/原shell有限核验需服务1.4.346:session_launch_reload_fence/session_shell_startup_wait。
|
|
73
|
+
只等同一承载的前台shell证明,不判断应用input-ready,不自动重试失败启动。
|
|
74
|
+
独立Agent数据目录可由管理员在profile配置nativeIdentity;需Linux及session_profile_native_identity。
|
|
72
75
|
get 可对账 tmux recovery 后仍指向旧 pane 的 IntegrationSession;需 Linux 服务1.4.328、session_recovery_binding_reconcile=true。
|
|
73
76
|
仅凭成功恢复记录、唯一归属/承载与当前精确 native writer-lock 证明更新 binding/generation;证据不足仍拒绝。
|
|
74
77
|
不另起 Agent、不重放已写入消息;成功后以 get 返回的 generation/terminal_binding 为准。
|
|
@@ -78,6 +78,16 @@ export const INTEGRATION_HELP = `td integration — local scoped integration adm
|
|
|
78
78
|
Prior versions cannot reconstruct missing launch evidence retroactively.
|
|
79
79
|
create --file <policy.json> --credential-file <private-output.json>
|
|
80
80
|
update --file <policy.json> (same identity/group; preserves the credential)
|
|
81
|
+
1.4.346 launch profiles may declare nativeIdentity={kind:"linux-flock-owner",
|
|
82
|
+
directory:"{cwd}/.agent-home/cli/thread-writer-locks"} for an isolated Agent home.
|
|
83
|
+
Linux only; session_profile_native_identity=true required on the running service.
|
|
84
|
+
This configures read-only identity proof; it does not set Agent environment,
|
|
85
|
+
install hooks, create directories, or modify old session snapshots.
|
|
86
|
+
session_launch_reload_fence/session_shell_startup_wait (minimum 1.4.346)
|
|
87
|
+
protect launch tokens during disk reload and wait at most 5s for the same
|
|
88
|
+
unique foreground shell. This does not wait for application input-ready.
|
|
89
|
+
diagnostics may include shell_check (reason/pane/pgid/tpgid/samples/waited_ms),
|
|
90
|
+
never shell argv/output. A timed-out launch does not automatically retry.
|
|
81
91
|
revoke <principal-id>
|
|
82
92
|
Credentials are saved as a private file and never printed. Workers set
|
|
83
93
|
TERMDOCK_INTEGRATION_CREDENTIAL_FILE, then use td collab --principal <id> …
|
|
@@ -248,6 +258,8 @@ export async function runIntegrationAdmin(argv, port, adminToken, write) {
|
|
|
248
258
|
throw new Error('update requires only --file');
|
|
249
259
|
const policy = JSON.parse(fs.readFileSync(options['--file'], 'utf8'));
|
|
250
260
|
const capabilities = parsed(await request(socketPath, 'GET', '/admin/capabilities', undefined, headers(undefined, adminToken)));
|
|
261
|
+
if (policy.launchProfiles?.some((profile) => profile.nativeIdentity !== undefined) && capabilities.session_profile_native_identity !== true)
|
|
262
|
+
throw new CollaborationError('SESSION_PROFILE_NATIVE_IDENTITY_UNSUPPORTED', 'CLI and Linux service must support profile native identity (minimum 1.4.346)', 409);
|
|
251
263
|
if (capabilities.integration_policy_update !== true || policy.launchProfiles?.some((profile) => profile.startupInput !== undefined) && capabilities.startup_input_conditions !== true)
|
|
252
264
|
throw new CollaborationError('STARTUP_INPUT_CONDITION_UNSUPPORTED', 'Running service does not support this policy update', 409);
|
|
253
265
|
write(JSON.stringify(parsed(await request(socketPath, 'POST', `/admin/principals/${encodeURIComponent(policy.id)}/policy`, policy, headers(undefined, adminToken)))));
|
|
@@ -257,9 +269,11 @@ export async function runIntegrationAdmin(argv, port, adminToken, write) {
|
|
|
257
269
|
if (!options['--file'] || !options['--credential-file'] || Object.keys(options).length !== 2)
|
|
258
270
|
throw new Error('create requires --file and --credential-file');
|
|
259
271
|
const policy = JSON.parse(fs.readFileSync(options['--file'], 'utf8'));
|
|
260
|
-
if (policy.launchProfiles?.some((profile) => profile.startupInput !== undefined)) {
|
|
272
|
+
if (policy.launchProfiles?.some((profile) => profile.startupInput !== undefined || profile.nativeIdentity !== undefined)) {
|
|
261
273
|
const capabilities = parsed(await request(socketPath, 'GET', '/admin/capabilities', undefined, headers(undefined, adminToken)));
|
|
262
|
-
if (capabilities.
|
|
274
|
+
if (policy.launchProfiles.some((profile) => profile.nativeIdentity !== undefined) && capabilities.session_profile_native_identity !== true)
|
|
275
|
+
throw new CollaborationError('SESSION_PROFILE_NATIVE_IDENTITY_UNSUPPORTED', 'CLI and Linux service must support profile native identity (minimum 1.4.346)', 409);
|
|
276
|
+
if (policy.launchProfiles.some((profile) => profile.startupInput !== undefined) && capabilities.startup_input_conditions !== true)
|
|
263
277
|
throw new CollaborationError('STARTUP_INPUT_CONDITION_UNSUPPORTED', 'Running service does not support startup input conditions', 409);
|
|
264
278
|
}
|
|
265
279
|
const destination = path.resolve(options['--credential-file']);
|
|
@@ -144,7 +144,7 @@ export class IntegrationRuntime {
|
|
|
144
144
|
app.post('/admin/principals/:id/policy', run(req => { admin(req); if (req.body?.id !== req.params.id)
|
|
145
145
|
integrationError('INVALID_INTEGRATION_POLICY', 'Policy identity must match the requested identity'); this.group(req.body?.groupId); this.store.update(req.body); return { ok: true, id: req.params.id }; }));
|
|
146
146
|
app.post('/admin/principals/:id/revoke', run(req => { admin(req); this.store.revoke(req.params.id); return { ok: true }; }));
|
|
147
|
-
app.get('/capabilities', run(req => { const p = principal(req); return { ...this.capabilities(), principal: { id: p.id, group_id: p.groupId, permissions: p.permissions, launch_profiles: p.launchProfiles.map(profile => ({ id: profile.id, agent_slug: profile.agentSlug, cwd_roots: profile.cwdRoots, startup_input_condition: !!profile.startupInput })) } }; }));
|
|
147
|
+
app.get('/capabilities', run(req => { const p = principal(req); return { ...this.capabilities(), principal: { id: p.id, group_id: p.groupId, permissions: p.permissions, launch_profiles: p.launchProfiles.map(profile => ({ id: profile.id, agent_slug: profile.agentSlug, cwd_roots: profile.cwdRoots, startup_input_condition: !!profile.startupInput, native_identity_scope: !!profile.nativeIdentity })) } }; }));
|
|
148
148
|
app.get('/tasks', run(req => { const p = principal(req); this.authorize(p, 'task.read', req.query.group); return { tasks: options.tasks.list(p.groupId, null, req.query.purpose) }; }));
|
|
149
149
|
app.get('/tasks/:id', run(async (req) => { const p = principal(req); this.taskPermission(p, req.params.id, 'task.read'); return { task: await options.tasks.get(req.params.id, null) }; }));
|
|
150
150
|
app.post('/tasks', run(async (req) => {
|
|
@@ -299,6 +299,9 @@ export class IntegrationRuntime {
|
|
|
299
299
|
terminal_input_runtime_fence: true, terminal_input_runtime_fence_min_version: '1.4.313',
|
|
300
300
|
agent_launch_generation_consistency: true, agent_launch_generation_consistency_min_version: '1.4.322',
|
|
301
301
|
terminal_input_check_diagnostics: true,
|
|
302
|
+
session_launch_reload_fence: true, session_launch_reload_fence_min_version: '1.4.346',
|
|
303
|
+
session_shell_startup_wait: true, session_shell_startup_wait_min_version: '1.4.346',
|
|
304
|
+
session_profile_native_identity: process.platform === 'linux', session_profile_native_identity_min_version: '1.4.346',
|
|
302
305
|
session_launch_diagnostics: true, session_launch_diagnostics_min_version: '1.4.314',
|
|
303
306
|
session_cold_retry: true, session_cold_retry_min_version: '1.4.314',
|
|
304
307
|
session_rename: true, session_rename_min_version: '1.4.315',
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CollaborationError } from './collaborationProtocol.js';
|
|
2
|
+
export class IntegrationShellWaitError extends CollaborationError {
|
|
3
|
+
shellEvidence;
|
|
4
|
+
constructor(code, shellEvidence) {
|
|
5
|
+
super(code, 'The original runtime did not prove a unique foreground shell before launch', 409);
|
|
6
|
+
this.shellEvidence = shellEvidence;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const samePane = (a, b) => a.serverPid === b.serverPid && a.sessionId === b.sessionId && a.paneId === b.paneId && a.panePid === b.panePid;
|
|
10
|
+
/** Wait for kernel/process evidence, never a prompt, fixed startup sleep, or
|
|
11
|
+
* application readiness. This observes one runtime and never sends input. */
|
|
12
|
+
export async function waitForIntegrationShell(input) {
|
|
13
|
+
const now = input.now ?? Date.now, started = now();
|
|
14
|
+
const pause = input.pause ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
15
|
+
let pinned = input.expected ?? null, samples = 0;
|
|
16
|
+
for (;;) {
|
|
17
|
+
const observation = await input.observe();
|
|
18
|
+
samples++;
|
|
19
|
+
const evidence = { samples, waited_ms: now() - started,
|
|
20
|
+
reason: observation.reason, binding: observation.binding, pgid: observation.pgid, tpgid: observation.tpgid };
|
|
21
|
+
if (!observation.binding || pinned && !samePane(pinned, observation.binding)) {
|
|
22
|
+
throw new IntegrationShellWaitError('SESSION_IDENTITY_MISMATCH', evidence);
|
|
23
|
+
}
|
|
24
|
+
pinned ??= observation.binding;
|
|
25
|
+
if (observation.agent)
|
|
26
|
+
throw new IntegrationShellWaitError('SESSION_TARGET_NOT_SHELL', evidence);
|
|
27
|
+
if (observation.confirmed)
|
|
28
|
+
return pinned;
|
|
29
|
+
if (now() - started >= (input.timeoutMs ?? 5000))
|
|
30
|
+
throw new IntegrationShellWaitError('SESSION_TARGET_NOT_SHELL', evidence);
|
|
31
|
+
await pause(Math.min(100, (input.timeoutMs ?? 5000) - (now() - started)));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
4
4
|
import { CollaborationError } from './collaborationProtocol.js';
|
|
5
|
+
import { resolveProfileNativeIdentity } from './nativeProcessIdentity.js';
|
|
5
6
|
export const INTEGRATION_PROTOCOL = 1;
|
|
6
7
|
export const INTEGRATION_OPERATIONS = ['message.send', 'message.read', 'task.read', 'task.create', 'task.configure', 'task.assign', 'task.comment', 'task.revise', 'task.answer', 'events.read', 'session.create', 'session.read', 'session.restore', 'session.release', 'session.rename', 'session.close'];
|
|
7
8
|
export function integrationError(code, message, status = 400) { throw new CollaborationError(code, message, status); }
|
|
@@ -54,6 +55,9 @@ export function validatePolicy(value) {
|
|
|
54
55
|
integrationError('INVALID_INTEGRATION_POLICY', 'resumeArgv requires one {sessionId} and one {launchArgs}; --last is forbidden');
|
|
55
56
|
if (!Array.isArray(profile.cwdRoots) || !profile.cwdRoots.length || profile.cwdRoots.some(root => typeof root !== 'string' || !path.isAbsolute(root) || /[\x00-\x1f\x7f]/.test(root)))
|
|
56
57
|
integrationError('INVALID_INTEGRATION_POLICY', 'cwdRoots must be absolute paths');
|
|
58
|
+
if (profile.nativeIdentity !== undefined && profile.cwdRoots.some(root => !resolveProfileNativeIdentity(profile.nativeIdentity, root))) {
|
|
59
|
+
integrationError('INVALID_INTEGRATION_POLICY', 'nativeIdentity must be an explicit owned-home writer-lock directory or a safe {cwd}/ relative template');
|
|
60
|
+
}
|
|
57
61
|
if (profile.startupInput !== undefined) {
|
|
58
62
|
const condition = profile.startupInput;
|
|
59
63
|
const validTerms = (terms) => Array.isArray(terms) && terms.length <= 16
|
|
@@ -14,6 +14,27 @@ export function nativeIdentityDirectory(config) {
|
|
|
14
14
|
const directory = path.resolve(expanded), relative = path.relative(os.homedir(), directory);
|
|
15
15
|
return relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) ? directory : null;
|
|
16
16
|
}
|
|
17
|
+
/** Administrator-declared profile scope, never inferred from process env.
|
|
18
|
+
* {cwd} is permitted only as a leading directory anchor and cannot escape it. */
|
|
19
|
+
export function resolveProfileNativeIdentity(config, cwd) {
|
|
20
|
+
if (!config || typeof config !== 'object' || Array.isArray(config))
|
|
21
|
+
return null;
|
|
22
|
+
const value = config;
|
|
23
|
+
if (value.kind !== 'linux-flock-owner' || typeof value.directory !== 'string'
|
|
24
|
+
|| Object.keys(value).some(key => !['kind', 'directory'].includes(key)))
|
|
25
|
+
return null;
|
|
26
|
+
let directory = value.directory;
|
|
27
|
+
if (directory.startsWith('{cwd}/')) {
|
|
28
|
+
const suffix = directory.slice(6);
|
|
29
|
+
if (/[{}]/.test(suffix) || suffix.split(/[\\/]/).includes('..') || !path.isAbsolute(cwd))
|
|
30
|
+
return null;
|
|
31
|
+
directory = path.join(cwd, suffix);
|
|
32
|
+
}
|
|
33
|
+
else if (/[{}]/.test(directory))
|
|
34
|
+
return null;
|
|
35
|
+
const normalized = nativeIdentityDirectory({ kind: value.kind, directory });
|
|
36
|
+
return normalized ? { kind: 'linux-flock-owner', directory: normalized } : null;
|
|
37
|
+
}
|
|
17
38
|
const unavailable = { nativeId: null, outcome: 'unavailable' };
|
|
18
39
|
function processStat(text, pid) {
|
|
19
40
|
const close = text.lastIndexOf(')'), open = text.indexOf('(');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CollaborationError } from './collaborationProtocol.js';
|
|
2
2
|
import { TerminalInputTargetChangedError } from './terminalInput.js';
|
|
3
|
+
import { IntegrationShellWaitError } from './integrationShellWait.js';
|
|
3
4
|
export function beginLaunchDiagnostics(operation_id, generation, kind) {
|
|
4
5
|
const now = Date.now();
|
|
5
6
|
return { operation_id, generation, kind, phase: 'configuration', updated_at: now,
|
|
@@ -22,5 +23,6 @@ export function launchFailure(current, error, error_code) {
|
|
|
22
23
|
: typeof value?.code === 'string' && standard.has(value.code) ? value.code
|
|
23
24
|
: typeof value?.message === 'string' && standard.has(value.message) ? value.message : 'UNCLASSIFIED_LAUNCH_ERROR';
|
|
24
25
|
return { ...current, updated_at: Date.now(), error_code, reason_code,
|
|
26
|
+
...(error instanceof IntegrationShellWaitError ? { shell_check: structuredClone(error.shellEvidence) } : {}),
|
|
25
27
|
...(error instanceof TerminalInputTargetChangedError ? { target_change: error.evidence } : {}) };
|
|
26
28
|
}
|
|
@@ -6,6 +6,7 @@ export function mergeSessionLaunchState(candidate, current, change) {
|
|
|
6
6
|
return current ? { ...candidate,
|
|
7
7
|
agentLaunchGeneration: current.agentLaunchGeneration ?? null,
|
|
8
8
|
agentLaunchCheck: current.agentLaunchCheck ?? null,
|
|
9
|
+
agentNativeIdentity: current.agentNativeIdentity ?? null,
|
|
9
10
|
} : candidate;
|
|
10
11
|
if (!current || (current.agentLaunchGeneration ?? null) !== change.expectedGeneration
|
|
11
12
|
|| (current.agentLaunchCheck?.id ?? null) !== change.expectedCheckId) {
|
|
@@ -16,5 +17,38 @@ export function mergeSessionLaunchState(candidate, current, change) {
|
|
|
16
17
|
check_phase: 'launch_state_commit', checked_at: Date.now(),
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
|
-
return { ...candidate, agentLaunchGeneration: change.generation, agentLaunchCheck: change.check
|
|
20
|
+
return { ...candidate, agentLaunchGeneration: change.generation, agentLaunchCheck: change.check,
|
|
21
|
+
agentNativeIdentity: change.nativeIdentity === undefined ? current.agentNativeIdentity ?? null : change.nativeIdentity };
|
|
22
|
+
}
|
|
23
|
+
/** A file watcher can finish reading an old snapshot after an in-memory launch
|
|
24
|
+
* or deletion. It must not revert server-owned fields or resurrect that ID. */
|
|
25
|
+
export function mergeReloadedSessionState(disk, beforeRead, current) {
|
|
26
|
+
const before = new Map(beforeRead.map(record => [record.sessionId, record]));
|
|
27
|
+
const live = new Map(current.map(record => [record.sessionId, record]));
|
|
28
|
+
const result = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const candidate of disk) {
|
|
31
|
+
const id = candidate.sessionId, now = live.get(id), old = before.get(id);
|
|
32
|
+
if (seen.has(id) || old && !now)
|
|
33
|
+
continue;
|
|
34
|
+
seen.add(id);
|
|
35
|
+
result.push(now ? JSON.stringify(now) !== JSON.stringify(old) ? now
|
|
36
|
+
: mergeSessionLaunchState(candidate, now) : candidate);
|
|
37
|
+
}
|
|
38
|
+
for (const record of current) {
|
|
39
|
+
if (!seen.has(record.sessionId) && (JSON.stringify(record) !== JSON.stringify(before.get(record.sessionId))
|
|
40
|
+
|| record.agentLaunchGeneration || record.agentNativeIdentity))
|
|
41
|
+
result.push(record);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
/** Serialize durable global snapshots, taking a fresh value when each write
|
|
46
|
+
* acquires the queue. A delayed old rename must not overwrite a later launch. */
|
|
47
|
+
export function createSessionStateWriter(current, write) {
|
|
48
|
+
let pending = Promise.resolve();
|
|
49
|
+
return () => {
|
|
50
|
+
const next = pending.catch(() => undefined).then(() => write(structuredClone(current())));
|
|
51
|
+
pending = next;
|
|
52
|
+
return next;
|
|
53
|
+
};
|
|
20
54
|
}
|
|
@@ -4,7 +4,7 @@ import { isConfirmedEmptyTmuxServer } from '../utils/tmuxEmptyServerProbe.js';
|
|
|
4
4
|
import { collaborationSessionDisplayName } from '../utils/sessionDisplayName.js';
|
|
5
5
|
import { integrationError } from '../agent/integrationStore.js';
|
|
6
6
|
import { assertNativeResumeAvailable, collectNativeResumeOwnerCandidates } from '../agent/nativeResumeOwner.js';
|
|
7
|
-
import { readNativeProcessIdentity } from '../agent/nativeProcessIdentity.js';
|
|
7
|
+
import { nativeIdentityDirectory, readNativeProcessIdentity, resolveProfileNativeIdentity } from '../agent/nativeProcessIdentity.js';
|
|
8
8
|
import { closeSessionRuntime, foregroundProcessInstance, processStartIdentity } from '../agent/sessionManagement.js';
|
|
9
9
|
import { releaseSessionRuntime } from '../agent/sessionLifecycle.js';
|
|
10
10
|
import { resumeConfigurationFingerprint } from '../agent/sessionResumeConfiguration.js';
|
|
@@ -59,7 +59,8 @@ import { assessAgentResourceBudget, collectHostResourceSnapshot, configuredAgent
|
|
|
59
59
|
import { AutomationStore, normalizeAutomationSchedule } from '../agent/automationStore.js';
|
|
60
60
|
import { writeBracketedPrompt, canDeliverPromptToAgent } from '../agent/promptDelivery.js';
|
|
61
61
|
import { captureTerminalInputGuard, captureRuntimeInputGuard, waitAndWriteTerminalInput } from '../agent/terminalInput.js';
|
|
62
|
-
import { mergeSessionLaunchState } from '../agent/sessionLaunchState.js';
|
|
62
|
+
import { createSessionStateWriter, mergeReloadedSessionState, mergeSessionLaunchState } from '../agent/sessionLaunchState.js';
|
|
63
|
+
import { waitForIntegrationShell, IntegrationShellWaitError } from '../agent/integrationShellWait.js';
|
|
63
64
|
import { agentLaunchBlockReason, normalizeAgentLaunchCheck } from '../agent/agentLaunchCheck.js';
|
|
64
65
|
import { prepareSupervisionEnable } from '../utils/enableSupervision.js';
|
|
65
66
|
import { buildServerHealthState, dismissServerHealth } from '../utils/serverHealth.js';
|
|
@@ -531,12 +532,14 @@ async function migrateFromClientStatesFile() {
|
|
|
531
532
|
return null;
|
|
532
533
|
}
|
|
533
534
|
}
|
|
534
|
-
async function loadGlobalSessionStateFromDisk() {
|
|
535
|
+
async function loadGlobalSessionStateFromDisk(reload = false) {
|
|
536
|
+
const beforeRead = structuredClone(globalSessionState.sessions);
|
|
535
537
|
try {
|
|
536
538
|
const data = await readJsonFileIfExists(GLOBAL_SESSION_STATE_FILE);
|
|
537
539
|
if (data) {
|
|
540
|
+
const sessions = deduplicateGlobalSessions((data.sessions || []).map(s => normalizePersistedClientSession(s)).filter((s) => s !== null));
|
|
538
541
|
globalSessionState = {
|
|
539
|
-
sessions:
|
|
542
|
+
sessions: reload ? mergeReloadedSessionState(sessions, beforeRead, globalSessionState.sessions) : sessions,
|
|
540
543
|
updatedAt: typeof data.updatedAt === 'number' ? data.updatedAt : Date.now(),
|
|
541
544
|
};
|
|
542
545
|
console.log(`[session-persist] Loaded ${globalSessionState.sessions.length} sessions from global state`);
|
|
@@ -587,7 +590,7 @@ async function persistTmuxRecoveryState() {
|
|
|
587
590
|
async function reloadGlobalSessionStateFromDisk(source) {
|
|
588
591
|
try {
|
|
589
592
|
const previous = JSON.stringify(globalSessionState);
|
|
590
|
-
await loadGlobalSessionStateFromDisk();
|
|
593
|
+
await loadGlobalSessionStateFromDisk(true);
|
|
591
594
|
const next = JSON.stringify(globalSessionState);
|
|
592
595
|
if (previous === next) {
|
|
593
596
|
return;
|
|
@@ -637,12 +640,13 @@ async function watchGlobalSessionStateFile() {
|
|
|
637
640
|
reportFailure('[session-persist] Failed to watch global state file:', error);
|
|
638
641
|
}
|
|
639
642
|
}
|
|
643
|
+
const writeGlobalSessionState = createSessionStateWriter(() => globalSessionState, snapshot => writeJsonFile(GLOBAL_SESSION_STATE_FILE, snapshot));
|
|
640
644
|
function schedulePersistGlobalState() {
|
|
641
645
|
if (persistGlobalStateTimer)
|
|
642
646
|
clearTimeout(persistGlobalStateTimer);
|
|
643
647
|
persistGlobalStateTimer = setTimeout(() => {
|
|
644
648
|
try {
|
|
645
|
-
void
|
|
649
|
+
void writeGlobalSessionState().catch((error) => {
|
|
646
650
|
console.warn('[session-persist] Failed to persist global state:', getErrorMessage(error));
|
|
647
651
|
});
|
|
648
652
|
}
|
|
@@ -657,7 +661,7 @@ async function persistGlobalStateNow(strict = false) {
|
|
|
657
661
|
persistGlobalStateTimer = null;
|
|
658
662
|
}
|
|
659
663
|
try {
|
|
660
|
-
await
|
|
664
|
+
await writeGlobalSessionState();
|
|
661
665
|
}
|
|
662
666
|
catch (error) {
|
|
663
667
|
console.warn('[session-persist] Failed to persist global state:', getErrorMessage(error));
|
|
@@ -1002,6 +1006,8 @@ function normalizePersistedClientSession(input) {
|
|
|
1002
1006
|
agentLaunchCheck: normalizeAgentLaunchCheck(candidate.agentLaunchCheck),
|
|
1003
1007
|
agentLaunchGeneration: typeof candidate.agentLaunchGeneration === 'string' ? candidate.agentLaunchGeneration
|
|
1004
1008
|
: normalizeAgentLaunchCheck(candidate.agentLaunchCheck)?.id ?? null,
|
|
1009
|
+
agentNativeIdentity: nativeIdentityDirectory(candidate.agentNativeIdentity)
|
|
1010
|
+
? { kind: 'linux-flock-owner', directory: nativeIdentityDirectory(candidate.agentNativeIdentity) } : null,
|
|
1005
1011
|
};
|
|
1006
1012
|
}
|
|
1007
1013
|
function generateTmuxSessionName() {
|
|
@@ -1485,14 +1491,18 @@ function searchMetadataForBackend(backendSessionId, session) {
|
|
|
1485
1491
|
updatedAt: session.lastActivity,
|
|
1486
1492
|
};
|
|
1487
1493
|
}
|
|
1488
|
-
async function beginAgentLaunchCheck(frontendSessionId, agentSlug, nativeSessionId = null) {
|
|
1494
|
+
async function beginAgentLaunchCheck(frontendSessionId, agentSlug, nativeSessionId = null, nativeIdentity) {
|
|
1489
1495
|
const record = globalSessionState.sessions.find(candidate => candidate.sessionId === frontendSessionId);
|
|
1490
1496
|
if (!record)
|
|
1491
1497
|
throw new Error('SESSION_REMOVED');
|
|
1492
1498
|
const id = randomUUID();
|
|
1493
1499
|
const updated = upsertGlobalSessionRecord(record, { expectedGeneration: record.agentLaunchGeneration ?? null,
|
|
1494
1500
|
expectedCheckId: record.agentLaunchCheck?.id ?? null, generation: id,
|
|
1495
|
-
check: { id, agentSlug, nativeSessionId, deadline: Date.now() + 120_000 }
|
|
1501
|
+
check: { id, agentSlug, nativeSessionId, deadline: Date.now() + 120_000 },
|
|
1502
|
+
nativeIdentity: nativeIdentity === undefined
|
|
1503
|
+
? record.agentResume?.slug === agentSlug || collaborationRouting.get(frontendSessionId)?.agentSlug === agentSlug
|
|
1504
|
+
? record.agentNativeIdentity ?? null : null
|
|
1505
|
+
: nativeIdentity });
|
|
1496
1506
|
// Capture the guard before persistence yields. The launch command must use
|
|
1497
1507
|
// this exact generation, even if another launch occurs during an await.
|
|
1498
1508
|
const assertCurrent = terminalInputGuard(updated);
|
|
@@ -1711,7 +1721,7 @@ function formatLocalCollaborationMessages(frontendSessionId, messages) {
|
|
|
1711
1721
|
* agent-keyed selector can never reach it. Delivery still needs an agent (the
|
|
1712
1722
|
* confirm gate and prompt formatting assume a TUI); driving a terminal —
|
|
1713
1723
|
* run/capture — is a shell operation and works on any pane. */
|
|
1714
|
-
async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane = false, nativeProcessIdentity = false, freshProcess = false, describeNativeFailure = false) {
|
|
1724
|
+
async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane = false, nativeProcessIdentity = false, freshProcess = false, describeNativeFailure = false, identityOverride) {
|
|
1715
1725
|
if (!binding.tmuxSessionName)
|
|
1716
1726
|
return { state: 'offline', reason: 'TMUX_BINDING_MISSING' };
|
|
1717
1727
|
const name = `=${binding.tmuxSessionName}`;
|
|
@@ -1737,7 +1747,7 @@ async function inspectCollaborationTmux(binding, requestedPane, allowPlainPane =
|
|
|
1737
1747
|
// pane_current_command alone cannot prove an exact resumed conversation.
|
|
1738
1748
|
const program = await resolveTmuxPaneProgram(pane, true, nativeProcessIdentity || freshProcess);
|
|
1739
1749
|
const agent = detectAgentFromCommand(program?.rawArgs ?? program?.command ?? '', agentCustomCommands());
|
|
1740
|
-
const identity = nativeProcessIdentity ? await observeProgramNativeIdentity(agent, program)
|
|
1750
|
+
const identity = nativeProcessIdentity ? await observeProgramNativeIdentity(agent, program, identityOverride ?? globalSessionState.sessions.find(record => record.sessionId === binding.sessionId)?.agentNativeIdentity ?? undefined)
|
|
1741
1751
|
: { nativeId: agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null, failure: null, source: null };
|
|
1742
1752
|
if (identity.failure && !describeNativeFailure)
|
|
1743
1753
|
integrationError(identity.failure === 'NATIVE_SESSION_IDENTITY_CONFLICT'
|
|
@@ -2050,6 +2060,37 @@ async function spawnCollaborationAgentSession(req, group, sourceSessionId, input
|
|
|
2050
2060
|
async function integrationPaneIsIdleShell(pane) {
|
|
2051
2061
|
return (await inspectIntegrationShell(pane)).confirmed;
|
|
2052
2062
|
}
|
|
2063
|
+
function integrationNativeIdentity(record) {
|
|
2064
|
+
if (!record.profile.nativeIdentity)
|
|
2065
|
+
return undefined;
|
|
2066
|
+
if (process.platform !== 'linux')
|
|
2067
|
+
integrationError('SESSION_PROFILE_NATIVE_IDENTITY_UNSUPPORTED', 'Profile writer-lock identity requires Linux', 409);
|
|
2068
|
+
const config = resolveProfileNativeIdentity(record.profile.nativeIdentity, record.cwd);
|
|
2069
|
+
if (!config)
|
|
2070
|
+
integrationError('INVALID_NATIVE_IDENTITY_CONFIG', 'Stored native identity scope is invalid', 409);
|
|
2071
|
+
return config;
|
|
2072
|
+
}
|
|
2073
|
+
async function waitForOwnedIntegrationShell(record, expected) {
|
|
2074
|
+
return waitForIntegrationShell({ expected, async observe() {
|
|
2075
|
+
const terminal = integrationTerminalRecord(record);
|
|
2076
|
+
if (!terminal)
|
|
2077
|
+
return { binding: null, confirmed: false, agent: false, reason: 'RUNTIME_NOT_REGISTERED', pgid: null, tpgid: null };
|
|
2078
|
+
const layout = await getTmuxLayout(terminal.tmuxSessionName);
|
|
2079
|
+
if (layout.windows.flatMap(window => window.panes).length !== 1) {
|
|
2080
|
+
return { binding: null, confirmed: false, agent: false, reason: 'MULTIPLE_TERMINAL_PANES', pgid: null, tpgid: null };
|
|
2081
|
+
}
|
|
2082
|
+
const inspected = await inspectCollaborationTmux({ sessionId: record.session_id, backendSessionId: terminal.backendSessionId,
|
|
2083
|
+
mode: 'tmux', tmuxSessionName: terminal.tmuxSessionName, agentSlug: null, nativeSessionId: null, pane: expected }, null, true, false, true);
|
|
2084
|
+
const pane = inspected.pane;
|
|
2085
|
+
if (inspected.state !== 'ready' || !pane)
|
|
2086
|
+
return { binding: null, confirmed: false, agent: false,
|
|
2087
|
+
reason: inspected.reason ?? 'PANE_UNCONFIRMED', pgid: null, tpgid: null };
|
|
2088
|
+
const shell = await inspectIntegrationShell(pane);
|
|
2089
|
+
return { binding: { serverPid: pane.serverPid, sessionId: pane.sessionId, paneId: pane.paneId, panePid: pane.panePid,
|
|
2090
|
+
agentSlug: '', nativeSessionId: null }, confirmed: shell.confirmed, agent: !!pane.agentSlug,
|
|
2091
|
+
reason: pane.isShell ? shell.failure : 'PANE_NOT_SHELL', pgid: shell.pgid, tpgid: shell.tpgid };
|
|
2092
|
+
} });
|
|
2093
|
+
}
|
|
2053
2094
|
async function inspectIntegrationShell(pane) {
|
|
2054
2095
|
if (!pane.isShell)
|
|
2055
2096
|
return { confirmed: false, failure: null, pgid: null, tpgid: null };
|
|
@@ -2157,7 +2198,7 @@ async function observeIntegrationCloseTarget(record) {
|
|
|
2157
2198
|
if (!record.agent_native_session_id || agent.slug !== record.agent_slug || program?.source !== 'tmux-tty' || !program.processPid) {
|
|
2158
2199
|
integrationError('SESSION_CLOSE_TARGET_UNCONFIRMED', 'A running Agent requires a live exact native identity', 409);
|
|
2159
2200
|
}
|
|
2160
|
-
const identity = await observeProgramNativeIdentity(agent, program);
|
|
2201
|
+
const identity = await observeProgramNativeIdentity(agent, program, integrationNativeIdentity(record));
|
|
2161
2202
|
if (identity.failure || identity.nativeId !== record.agent_native_session_id)
|
|
2162
2203
|
integrationError('SESSION_IDENTITY_MISMATCH', 'Current Agent native identity differs from the owned record', 409);
|
|
2163
2204
|
kind = 'agent';
|
|
@@ -2246,7 +2287,7 @@ export const integrationSessionAdapter = {
|
|
|
2246
2287
|
if (!agent || !command)
|
|
2247
2288
|
integrationError('EXACT_RESUME_UNSUPPORTED', 'Agent must support exact native recovery', 409);
|
|
2248
2289
|
return { version: 1, slug: agent.slug, aliases: agent.aliases, capabilities: agent.capabilities ?? [],
|
|
2249
|
-
resume: getPluginResumeConfig(agent.slug) ?? command, nativeIdentity: getPluginNativeIdentityConfig(agent.slug) ?? null };
|
|
2290
|
+
resume: getPluginResumeConfig(agent.slug) ?? command, nativeIdentity: integrationNativeIdentity(record) ?? getPluginNativeIdentityConfig(agent.slug) ?? null };
|
|
2250
2291
|
},
|
|
2251
2292
|
async runtimePresent(record) {
|
|
2252
2293
|
if (integrationTerminalRecord(record))
|
|
@@ -2303,7 +2344,7 @@ export const integrationSessionAdapter = {
|
|
|
2303
2344
|
const binding = { sessionId: record.session_id, backendSessionId: terminal.backendSessionId,
|
|
2304
2345
|
mode: 'tmux', tmuxSessionName: terminal.tmuxSessionName, agentSlug: record.agent_slug,
|
|
2305
2346
|
nativeSessionId: record.agent_native_session_id, pane: record.terminal_binding ?? null };
|
|
2306
|
-
const inspected = await inspectCollaborationTmux(binding, undefined, false, true);
|
|
2347
|
+
const inspected = await inspectCollaborationTmux(binding, undefined, false, true, false, false, integrationNativeIdentity(record));
|
|
2307
2348
|
if (inspected.state === 'offline')
|
|
2308
2349
|
return { exists: false, running: false, shell: false, agentSlug: null, nativeId: null };
|
|
2309
2350
|
if (inspected.state !== 'ready' || !inspected.pane)
|
|
@@ -2443,7 +2484,7 @@ async function assertIntegrationNativeResumeAvailable(record) {
|
|
|
2443
2484
|
return Promise.all(layout.windows.flatMap(window => window.panes).map(async (pane) => {
|
|
2444
2485
|
const program = await resolveTmuxPaneProgram(pane, true, true);
|
|
2445
2486
|
const agent = detectAgentFromCommand(program?.rawArgs ?? program?.command ?? '', agentCustomCommands());
|
|
2446
|
-
const identity = await observeProgramNativeIdentity(agent, program);
|
|
2487
|
+
const identity = await observeProgramNativeIdentity(agent, program, globalSessionState.sessions.find(record => record.sessionId === candidate.sessionId)?.agentNativeIdentity ?? integrationNativeIdentity(record));
|
|
2447
2488
|
const shell = await inspectIntegrationShell({ panePid: pane.pid,
|
|
2448
2489
|
isShell: !agent && shellNamesBackend.has(normalizeProgramName(program?.command ?? '')?.toLowerCase() ?? '') });
|
|
2449
2490
|
return { confirmed: !identity.failure && (shell.confirmed || program?.source === 'tmux-tty' && Boolean(program.rawArgs)),
|
|
@@ -2512,13 +2553,17 @@ async function launchIntegrationTerminal(record, restoring, prepared, progress,
|
|
|
2512
2553
|
mode: 'tmux', tmuxSessionName: terminal.tmuxSessionName, agentSlug: null, nativeSessionId: null,
|
|
2513
2554
|
// A recreated tmux may get a new pane only during explicit exact resume.
|
|
2514
2555
|
pane: original.exists ? record.terminal_binding ?? null : null }, null, true);
|
|
2515
|
-
if (inspected.state !== 'ready' || !inspected.pane
|
|
2516
|
-
|
|
2556
|
+
if (inspected.state !== 'ready' || !inspected.pane)
|
|
2557
|
+
throw new IntegrationShellWaitError('SESSION_TARGET_NOT_SHELL', {
|
|
2558
|
+
samples: 1, waited_ms: 0, reason: inspected.reason ?? 'PANE_UNCONFIRMED', binding: null, pgid: null, tpgid: null
|
|
2559
|
+
});
|
|
2517
2560
|
const pane = inspected.pane;
|
|
2518
2561
|
const pinned = { serverPid: pane.serverPid, sessionId: pane.sessionId,
|
|
2519
2562
|
paneId: pane.paneId, panePid: pane.panePid, agentSlug: record.agent_slug, nativeSessionId: record.agent_native_session_id };
|
|
2520
2563
|
progress?.('binding_persist');
|
|
2521
2564
|
prepared(pinned); // Must persist before the command enters the terminal.
|
|
2565
|
+
progress?.('runtime_shell_wait');
|
|
2566
|
+
await waitForOwnedIntegrationShell(record, pinned);
|
|
2522
2567
|
collaborationRouting.bind({ sessionId: record.session_id, backendSessionId: opened.terminalSession.sessionId,
|
|
2523
2568
|
mode: 'tmux', tmuxSessionName: terminal.tmuxSessionName, agentSlug: record.agent_slug, nativeSessionId: record.agent_native_session_id, pane: pinned });
|
|
2524
2569
|
const latest = collaborationStore.getGroup(group.id);
|
|
@@ -2540,17 +2585,14 @@ async function launchIntegrationTerminal(record, restoring, prepared, progress,
|
|
|
2540
2585
|
await assertIntegrationNativeResumeAvailable(record);
|
|
2541
2586
|
}
|
|
2542
2587
|
progress?.('runtime_verify');
|
|
2543
|
-
|
|
2544
|
-
mode: 'tmux', tmuxSessionName: terminal.tmuxSessionName, agentSlug: null, nativeSessionId: null, pane: pinned });
|
|
2545
|
-
if (shell.state !== 'ready' || !shell.pane || !await integrationPaneIsIdleShell(shell.pane))
|
|
2546
|
-
integrationError('SESSION_TARGET_NOT_SHELL', 'Owned shell changed before launch', 409);
|
|
2588
|
+
await waitForOwnedIntegrationShell(record, pinned);
|
|
2547
2589
|
// This owned pane is a verified shell; clear stale rich state before a new
|
|
2548
2590
|
// process starts, so its new SessionStart must supply the native binding.
|
|
2549
2591
|
backend.agentSession = null;
|
|
2550
2592
|
backend.agent = null;
|
|
2551
2593
|
backend.agentResumeRecovered = false;
|
|
2552
2594
|
progress?.('launch_intent');
|
|
2553
|
-
const launchGuard = await beginAgentLaunchCheck(record.session_id, record.agent_slug, restoring ? record.agent_native_session_id : null);
|
|
2595
|
+
const launchGuard = await beginAgentLaunchCheck(record.session_id, record.agent_slug, restoring ? record.agent_native_session_id : null, integrationNativeIdentity(record) ?? null);
|
|
2554
2596
|
await writeAutomaticTerminalPrompt(record.session_id, integrationLaunchCommand(record, restoring), progress, launchGuard);
|
|
2555
2597
|
broadcastClientState();
|
|
2556
2598
|
});
|
|
@@ -4532,9 +4574,9 @@ async function resolveTmuxPaneProgram(pane, requireArgs = false, refreshTty = fa
|
|
|
4532
4574
|
}
|
|
4533
4575
|
return { command, source: 'tmux-pane', rawArgs: null };
|
|
4534
4576
|
}
|
|
4535
|
-
async function observeProgramNativeIdentity(agent, program) {
|
|
4577
|
+
async function observeProgramNativeIdentity(agent, program, override) {
|
|
4536
4578
|
const fromArgs = agent && program?.rawArgs ? inferResumeSessionId(agent, splitCommandToArgv(program.rawArgs)) : null;
|
|
4537
|
-
const config = agent ? getPluginNativeIdentityConfig(agent.slug) : undefined;
|
|
4579
|
+
const config = agent ? override ?? getPluginNativeIdentityConfig(agent.slug) : undefined;
|
|
4538
4580
|
if (config && program?.processPid) {
|
|
4539
4581
|
const proof = await readNativeProcessIdentity(program.processPid, config, program.processComm, program.rawArgs ?? undefined);
|
|
4540
4582
|
if (proof.outcome === 'ambiguous' || proof.outcome === 'process_changed' || proof.outcome === 'unconfirmed')
|
|
@@ -4546,7 +4588,11 @@ async function observeProgramNativeIdentity(agent, program) {
|
|
|
4546
4588
|
return { nativeId: null, source: null, failure: 'NATIVE_SESSION_IDENTITY_CONFLICT' };
|
|
4547
4589
|
return { nativeId: proof.nativeId, source: 'linux-flock-owner', failure: null };
|
|
4548
4590
|
}
|
|
4591
|
+
if (override)
|
|
4592
|
+
return { nativeId: null, source: null, failure: 'NATIVE_SESSION_OWNERSHIP_UNCONFIRMED' };
|
|
4549
4593
|
}
|
|
4594
|
+
if (override && agent)
|
|
4595
|
+
return { nativeId: null, source: null, failure: 'NATIVE_SESSION_OWNERSHIP_UNCONFIRMED' };
|
|
4550
4596
|
return { nativeId: fromArgs, source: fromArgs ? 'argv' : null, failure: null };
|
|
4551
4597
|
}
|
|
4552
4598
|
/**
|
package/docs/integrations.md
CHANGED
|
@@ -1354,3 +1354,32 @@ flock-owner 身份独立核验出原 agent slug 与原生 UUID。目标元组会
|
|
|
1354
1354
|
fail-closed,旧绑定与原消息保留,`session get` 仍报告身份错误;不会按 tmux 名称、PID
|
|
1355
1355
|
缓存、cwd、argv 中的 resume 参数或显示名猜测认领。此 capability 不新增权限;仅通过
|
|
1356
1356
|
`session.read` 认证的所属 principal 可触发其自身记录的对账。
|
|
1357
|
+
|
|
1358
|
+
## 1.4.346:启动磁盘重载、原 shell 核验与独立原生目录
|
|
1359
|
+
|
|
1360
|
+
最低 CLI / 实际服务均为 **1.4.346**。通过所属 principal 的 `capabilities` 回读:
|
|
1361
|
+
|
|
1362
|
+
- `session_launch_reload_fence=true`:服务运行期间的磁盘重载保留当前服务生成的 launch token、已解除的一次性 check 和显式原生目录。读取期间新增的会话不会消失,已删除的会话不会由旧读盘复活;全局状态写入串行,并在执行写入时采集当前快照。服务首次启动仍加载磁盘记录。这修复了磁盘 watch 路径绕过元数据 upsert 保护的缺口,不把每一例历史 `TERMINAL_INPUT_TARGET_CHANGED` 都归因为此。
|
|
1363
|
+
- `session_shell_startup_wait=true`:启动前先记录唯一原 pane 身份,再最多等待 5 秒核实该 shell 的前台关系和合格 shell 参数。只观察同一 tmux server/session/pane/PID,采样使用新鲜进程证据;承载改变、pane 不唯一或已有 Agent 时拒绝。不输入探针、不清理画面、不重建承载、不发送额外回车,也不把 shell 核验当成应用 input-ready。
|
|
1364
|
+
- `session_profile_native_identity=true`:当前仅 Linux 支持 profile 级只读 writer-lock 目录,覆盖该受管会话的插件缺省目录,不改变共享插件配置。三个字段分别有同名 `_min_version` 字段,值为 `1.4.346`。能力支持不等于 principal 已被授予权限。
|
|
1365
|
+
|
|
1366
|
+
本机管理员可在现有完整 policy 的目标 `launchProfiles` 中加:
|
|
1367
|
+
|
|
1368
|
+
```json
|
|
1369
|
+
"nativeIdentity": {
|
|
1370
|
+
"kind": "linux-flock-owner",
|
|
1371
|
+
"directory": "{cwd}/.agent-home/cli/thread-writer-locks"
|
|
1372
|
+
}
|
|
1373
|
+
```
|
|
1374
|
+
|
|
1375
|
+
目录是示例,必须按实际 Agent 的持锁协议填写。支持管理员指定的绝对目录、`~/` 目录,以及仅以前缀 `{cwd}/` 锚定的模板;模板不允许 `..` 或其它变量,解析后的目录须在 TD 服务用户 home 内。调用现有 `td integration update --file <完整policy.json>`,不增加权限、不重建 principal 或凭据。CLI 在旧服务/非 Linux 服务上拒绝该配置,不能静默忽略。原有不配置此项的 profile 保持插件缺省行为。
|
|
1376
|
+
|
|
1377
|
+
wrapper 负责实际设置 Agent 自己的数据目录、准备 hooks/参数,并最终运行真正的 Agent;这个字段只声明 TD 的核验目录,不设置环境、不执行脚本、不创建目录,不自动读取 `TRAECLI_HOME` 等私有变量。TD 仍检查目录与文件权限、进程 starttime、前台关系、实际 FD/inode 和当前内核 flock,要求唯一 native UUID。显式 profile 目录不可证明时不降级到 argv/hook/旧 UUID,多个持锁 UUID 仍拒绝。目录语义纳入恢复配置指纹,保存至启动代次;服务重启、metadata 更新、check 解除不会丢失它。
|
|
1378
|
+
|
|
1379
|
+
该配置只用于之后创建的 session;旧 session 保留原 profile 快照,policy update 不迁移旧记录或修改运行 Agent。不要为旧未知结果自动 create 新 ID 或 retry-cold。原 UUID 恢复、重复 owner 保护、关闭仍遵守现有身份/代次/幂等门禁;独立数据目录不构成对同 UID 进程的安全隔离。
|
|
1380
|
+
|
|
1381
|
+
启动失败管理员用 `td integration diagnostics <原TD-ID>` 回读;shell 等待失败新增 `shell_check={samples,waited_ms,reason,binding,pgid,tpgid}`,不包含 argv/env/终端正文。`SESSION_TARGET_NOT_SHELL` 表示有限核验未通过,不表示 wrapper 已执行;`paste/submit=not_attempted` 与后续 `runtime_present` 现场回读分开保存。失败不自动再次启动,只有符合原 `session retry-cold` 所有条件才可明确重试。
|
|
1382
|
+
|
|
1383
|
+
用 stdin 文件给启动命令附带业务 prompt 是接入方的 bootstrap:TD 不为其中业务副作用提供消息幂等或执行确认;无 TD message 历史也不等于没有业务执行。冷启动与精确 resume 应分开处理,恢复时不得重放初始业务文件。host 收取 final/hook receipt 是 host 侧证据,不伪造成 TD 明确 reply。仍只粘贴一次、等待 50ms、写一次 CR。
|
|
1384
|
+
|
|
1385
|
+
回归包含确定性交错的旧读盘覆盖/串行写入/删除不复活、同 pane shell 初始化等待与超时/换 pane/已有 Agent 拒绝、CLI 对旧服务拒绝,以及 Linux 真实 tmux/内核 flock 的独立目录核验。私有 TraeX/DevBox 的启动兼容仍需接入方维护窗实机验证;维护机合成测试不冒充该项通过。
|
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.346",
|
|
5
5
|
"runtimeProtocolVersion": 1,
|
|
6
6
|
"minimumDesktopVersion": "1.4.46",
|
|
7
7
|
"nodeMajor": 22,
|
|
8
8
|
"dependencyHash": "sha256-ObG1OGq7e7fMFQmYoPxPp3jRodEusYHulVNa5mT1aW8=",
|
|
9
|
-
"serverBundleHash": "sha256-
|
|
9
|
+
"serverBundleHash": "sha256-14J5QvRzetON18qbhx5HXdLAx08WrjndjMlOCMpgfOc=",
|
|
10
10
|
"clientBundleHash": "sha256-kn3Tg+BTj+H4rZe0g9OE5JUEQNsBGiUSGew8Epm5A38=",
|
|
11
11
|
"entrypoint": "dist/server/cli.js",
|
|
12
12
|
"clientEntrypoint": "dist/client/index.html"
|