shennian 0.3.85 → 0.3.86
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/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +28 -28
- package/dist/src/agents/adapter.js +19 -1
- package/dist/src/agents/claude-managed-permissions.js +248 -1
- package/dist/src/agents/claude.js +393 -8
- package/dist/src/agents/codex-managed-permissions.js +294 -1
- package/dist/src/agents/codex-managed-room-tools.js +249 -1
- package/dist/src/agents/codex-utils.js +200 -7
- package/dist/src/agents/codex.js +1352 -17
- package/dist/src/agents/config-status.js +226 -1
- package/dist/src/agents/cursor-managed-permissions.js +299 -2
- package/dist/src/agents/cursor.js +314 -1
- package/dist/src/agents/custom.js +418 -4
- package/dist/src/agents/external-channel-instructions.js +94 -11
- package/dist/src/agents/gemini.js +180 -1
- package/dist/src/agents/manager.d.ts +2 -0
- package/dist/src/agents/manager.js +250 -18
- package/dist/src/agents/model-registry/cache.js +37 -1
- package/dist/src/agents/model-registry/discovery.js +188 -2
- package/dist/src/agents/model-registry/parsers.js +521 -4
- package/dist/src/agents/model-registry/runner.js +30 -1
- package/dist/src/agents/model-registry/service.js +115 -1
- package/dist/src/agents/model-registry/types.js +8 -1
- package/dist/src/agents/model-registry.js +19 -1
- package/dist/src/agents/native-capabilities.js +237 -1
- package/dist/src/agents/openclaw.js +293 -2
- package/dist/src/agents/opencode-managed-permissions.js +187 -2
- package/dist/src/agents/opencode.js +298 -1
- package/dist/src/agents/platform-instructions.js +46 -17
- package/dist/src/agents/runtime-compatibility.js +44 -1
- package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
- package/dist/src/agents/workbuddy.js +444 -3
- package/dist/src/commands/room.js +2 -2
- package/dist/src/fs/boundary.js +185 -1
- package/dist/src/fs/handler.js +130 -1
- package/dist/src/fs/security.js +32 -1
- package/dist/src/fs/text-decoder.js +110 -1
- package/dist/src/index.js +587 -3
- package/dist/src/manager/prompt.js +34 -29
- package/dist/src/manager/registry.js +317 -1
- package/dist/src/manager/retired-external-channel-compat.js +40 -1
- package/dist/src/manager/runtime.d.ts +4 -0
- package/dist/src/manager/runtime.js +1514 -23
- package/dist/src/native-fusion/codex-parser.js +660 -4
- package/dist/src/native-fusion/codex-user-input.js +102 -5
- package/dist/src/native-fusion/config.js +5 -1
- package/dist/src/native-fusion/file-watcher.js +191 -1
- package/dist/src/native-fusion/local-repository.js +569 -2
- package/dist/src/native-fusion/opencode-parser.js +130 -3
- package/dist/src/native-fusion/parsers.js +140 -3
- package/dist/src/native-fusion/service.js +739 -2
- package/dist/src/native-fusion/session-header.js +101 -2
- package/dist/src/native-fusion/state.js +104 -2
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/personal-sync/machine-authority.js +34 -1
- package/dist/src/room/activation-audit.js +45 -2
- package/dist/src/room/activation-policy.js +30 -1
- package/dist/src/room/activation-service.d.ts +1 -0
- package/dist/src/room/activation-service.js +408 -8
- package/dist/src/room/invite-bootstrap.js +156 -1
- package/dist/src/room/managed-context.d.ts +1 -1
- package/dist/src/room/managed-context.js +1 -1
- package/dist/src/room/managed-prompt-composer.d.ts +1 -1
- package/dist/src/room/managed-prompt-composer.js +6 -5
- package/dist/src/room/managed-room-binding.js +167 -1
- package/dist/src/room/managed-room-ipc.d.ts +2 -2
- package/dist/src/room/managed-room-ipc.js +2 -2
- package/dist/src/room/mirror-sync-service.js +183 -1
- package/dist/src/room/room-action-client.d.ts +11 -0
- package/dist/src/room/room-action-client.js +1 -1
- package/dist/src/room/room-binding-client.d.ts +1 -0
- package/dist/src/room/room-binding-client.js +151 -1
- package/dist/src/security/managed-agent-policy.d.ts +3 -2
- package/dist/src/security/managed-agent-policy.js +1 -1
- package/dist/src/session/archive-zip.js +220 -1
- package/dist/src/session/deletion-sync-state.js +136 -2
- package/dist/src/session/handlers/agent-capabilities.js +14 -1
- package/dist/src/session/handlers/agent-config.js +150 -1
- package/dist/src/session/handlers/agents.js +55 -1
- package/dist/src/session/handlers/chat.d.ts +1 -1
- package/dist/src/session/handlers/chat.js +1030 -2
- package/dist/src/session/handlers/control.js +61 -1
- package/dist/src/session/handlers/fs.js +594 -1
- package/dist/src/session/handlers/message-status.js +15 -1
- package/dist/src/session/handlers/remote-access.js +43 -1
- package/dist/src/session/handlers/room-managed.js +101 -1
- package/dist/src/session/handlers/session-refresh.js +35 -1
- package/dist/src/session/handlers/title.js +79 -1
- package/dist/src/session/handlers/tool-detail.js +149 -1
- package/dist/src/session/handlers/voice-window.js +62 -1
- package/dist/src/session/history-subscriptions.js +470 -1
- package/dist/src/session/manager.js +658 -1
- package/dist/src/session/native-cleanup-outbox.js +287 -2
- package/dist/src/session/native-session-lifecycle.js +327 -6
- package/dist/src/session/queue.d.ts +5 -0
- package/dist/src/session/queue.js +8 -8
- package/dist/src/upgrade/scheduler.js +126 -1
- package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
- package/package.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -1,3 +1,587 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// @arch docs/architecture/cli/daemon.md
|
|
2
|
+
// @test src/__tests__/agents-e2e.ts
|
|
3
|
+
// @test src/__tests__/daemon-autostart.test.ts
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import { loadConfig, saveConfig, getConfigPath, getShennianDir, resolveShennianPath, normalizeAutoUpgradePolicy, } from './config/index.js';
|
|
9
|
+
import { CliRelayClient } from './relay/client.js';
|
|
10
|
+
import { collectKnownLogSecrets, installCliLogRedaction, sanitizeCliLogValue, } from './log-redaction.js';
|
|
11
|
+
import { registerPairCommand, runSmartStart } from './commands/pair.js';
|
|
12
|
+
import { clearDaemonPidIfOwner, createDaemonInstanceId, clearDaemonLauncher, findRunningDaemonProcessIds, isRemoteAccessDisabled, prepareRemoteAccessPause, stopPreparedRemoteAccessService, isIsolatedDaemonE2eInstance, registerDaemonCommand, writeDaemonPid, writeDaemonLauncher, } from './commands/daemon.js';
|
|
13
|
+
import { registerRuntimeCommand } from './commands/runtime.js';
|
|
14
|
+
import { registerManagerCommand } from './commands/manager.js';
|
|
15
|
+
import { registerUpgradeCommand } from './commands/upgrade.js';
|
|
16
|
+
import { registerToolsCommand } from './commands/tools.js';
|
|
17
|
+
import { registerIntegrationCommand } from './commands/integration.js';
|
|
18
|
+
import { registerRoomCommand } from './commands/room.js';
|
|
19
|
+
import { SessionManager } from './session/manager.js';
|
|
20
|
+
import { SERVERS, regionToUrl, urlToRegion } from './region.js';
|
|
21
|
+
import { getCurrentVersion, handleStartupCrashCheck } from './upgrade/engine.js';
|
|
22
|
+
import { AutoUpgradeScheduler } from './upgrade/scheduler.js';
|
|
23
|
+
import { detectAgents } from './agents/detect.js';
|
|
24
|
+
import { augmentProcessPath } from './env-path.js';
|
|
25
|
+
augmentProcessPath();
|
|
26
|
+
const cliVersion = getCurrentVersion();
|
|
27
|
+
import { getCachedAgentInfos, resolveAgentInfos } from './agents/model-registry.js';
|
|
28
|
+
import { initCliLogReporter, reportLog } from './log-reporter.js';
|
|
29
|
+
import { isNativeFusionEnabled } from './native-fusion/config.js';
|
|
30
|
+
import { NativeSessionFusionService } from './native-fusion/service.js';
|
|
31
|
+
import { startDaemonLogRetention } from './daemon-log.js';
|
|
32
|
+
import { startDaemonIpcServer } from './daemon-ipc/server.js';
|
|
33
|
+
import { resolveManagedRoomIpcPaths } from './daemon-ipc/paths.js';
|
|
34
|
+
import { RoomSessionRegistry } from './room/session-registry.js';
|
|
35
|
+
import { HttpRoomBindingClient } from './room/room-binding-client.js';
|
|
36
|
+
import { TrustedManagedRoomBinder } from './room/managed-room-binding.js';
|
|
37
|
+
import { RoomHostSessionObservationStore } from './room/host-session-observations.js';
|
|
38
|
+
import { RoomMirrorSyncService } from './room/mirror-sync-service.js';
|
|
39
|
+
import { RoomActivationService } from './room/activation-service.js';
|
|
40
|
+
import { RoomActivationAudit } from './room/activation-audit.js';
|
|
41
|
+
import { RoomAttachmentCache } from './room/attachment-cache.js';
|
|
42
|
+
import { RoomAttachmentUploader } from './room/attachment-upload.js';
|
|
43
|
+
import { ManagedRoomContextAuthority } from './room/managed-context.js';
|
|
44
|
+
import { createManagedRoomExecutor } from './room/managed-room-ipc.js';
|
|
45
|
+
import { SESSION_SYNC_PROTOCOL_VERSION, SESSION_SYNC_ADVERTISED_CAPABILITIES } from '@shennian/wire';
|
|
46
|
+
import { getDaemonInstallationId } from './session/store.js';
|
|
47
|
+
import { synchronizeMachinePersonalAuthority } from './personal-sync/machine-authority.js';
|
|
48
|
+
import { CLI_SESSION_SYNC_DATA_EPOCH, ensurePersonalDataEpoch } from './personal-sync/epoch.js';
|
|
49
|
+
import { DeletionSyncRetryWaitError, DeletionSyncTerminalError, } from './session/deletion-sync-state.js';
|
|
50
|
+
const SHENNIAN_DIR = getShennianDir();
|
|
51
|
+
const PID_FILE = resolveShennianPath('daemon.pid');
|
|
52
|
+
function readDaemonPidFile() {
|
|
53
|
+
try {
|
|
54
|
+
const raw = fs.readFileSync(PID_FILE, 'utf-8').trim();
|
|
55
|
+
if (raw.startsWith('{')) {
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
const pid = Number(parsed.pid);
|
|
58
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
59
|
+
}
|
|
60
|
+
const pid = parseInt(raw, 10);
|
|
61
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function httpToWs(url) {
|
|
68
|
+
return url.replace(/^https:\/\//, 'wss://').replace(/^http:\/\//, 'ws://');
|
|
69
|
+
}
|
|
70
|
+
function writeRetiredIntegrationError() {
|
|
71
|
+
process.stdout.write(`${JSON.stringify({
|
|
72
|
+
ok: false,
|
|
73
|
+
error: 'integration_command_retired',
|
|
74
|
+
message: 'Room MCP and Hooks were retired. Use the Shennian Skill and CLI.',
|
|
75
|
+
nextAction: { type: 'open_install_guide', url: 'https://shennian.net/install.md' },
|
|
76
|
+
})}\n`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
}
|
|
79
|
+
function formatDisconnectInfo(info) {
|
|
80
|
+
const parts = [
|
|
81
|
+
`trigger=${info.trigger ?? 'unknown'}`,
|
|
82
|
+
`phase=${info.phase}`,
|
|
83
|
+
`code=${info.code ?? 'unknown'}`,
|
|
84
|
+
`attempt=${info.reconnectAttempt}`,
|
|
85
|
+
];
|
|
86
|
+
if (info.reason)
|
|
87
|
+
parts.push(`reason=${info.reason}`);
|
|
88
|
+
if (info.error)
|
|
89
|
+
parts.push(`error=${info.error}`);
|
|
90
|
+
return parts.join(' ');
|
|
91
|
+
}
|
|
92
|
+
async function waitForPidExit(pid, timeoutMs = 5000) {
|
|
93
|
+
const startedAt = Date.now();
|
|
94
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
95
|
+
try {
|
|
96
|
+
process.kill(pid, 0);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
process.kill(pid, 0);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const program = new Command();
|
|
112
|
+
program.name('shennian').description('Shennian — AI Agent Control Plane').version(cliVersion);
|
|
113
|
+
// Keep options written after a subcommand scoped to that subcommand. In particular,
|
|
114
|
+
// `room create --name` must not be consumed by the legacy root machine-name option.
|
|
115
|
+
program.enablePositionalOptions();
|
|
116
|
+
registerIntegrationCommand(program);
|
|
117
|
+
registerRoomCommand(program);
|
|
118
|
+
program
|
|
119
|
+
.option('--api <url>', 'Server URL override')
|
|
120
|
+
.option('--name <name>', 'Machine name', os.hostname())
|
|
121
|
+
.action(async (opts) => {
|
|
122
|
+
const config = loadConfig();
|
|
123
|
+
const serverUrl = opts.api ?? process.env.SHENNIAN_DESKTOP_SERVER_URL ?? config.serverUrl ?? undefined;
|
|
124
|
+
await runSmartStart(serverUrl, opts.name);
|
|
125
|
+
});
|
|
126
|
+
// run-service: internal command used by the background service process
|
|
127
|
+
program
|
|
128
|
+
.command('run-service', { hidden: true })
|
|
129
|
+
.description('(internal) Connect to relay server, called by the background service')
|
|
130
|
+
.option('--api <url>', 'Server URL override')
|
|
131
|
+
.action(async (opts) => {
|
|
132
|
+
const envFile = resolveShennianPath('env.json');
|
|
133
|
+
try {
|
|
134
|
+
const saved = JSON.parse(fs.readFileSync(envFile, 'utf-8'));
|
|
135
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
136
|
+
if (!process.env[k])
|
|
137
|
+
process.env[k] = v;
|
|
138
|
+
}
|
|
139
|
+
augmentProcessPath();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// env.json may not exist yet
|
|
143
|
+
}
|
|
144
|
+
installCliLogRedaction(collectKnownLogSecrets(process.env, loadConfig()));
|
|
145
|
+
const logRetentionTimer = startDaemonLogRetention();
|
|
146
|
+
if (isRemoteAccessDisabled()) {
|
|
147
|
+
console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`);
|
|
148
|
+
clearInterval(logRetentionTimer);
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
// Single-instance guard. Service-manager starts are authoritative and may
|
|
152
|
+
// need to take over from an older detached process after app/daemon upgrades.
|
|
153
|
+
const serviceManagedStart = Boolean(process.env.INVOCATION_ID ||
|
|
154
|
+
process.env.JOURNAL_STREAM ||
|
|
155
|
+
process.env.SHENNIAN_DESKTOP_SERVER_URL);
|
|
156
|
+
const stopExistingDaemon = async (oldPid, reason) => {
|
|
157
|
+
console.log(`[${new Date().toISOString()}] ${reason} (PID ${oldPid})`);
|
|
158
|
+
process.kill(oldPid, 'SIGTERM');
|
|
159
|
+
const stopped = await waitForPidExit(oldPid);
|
|
160
|
+
if (!stopped) {
|
|
161
|
+
process.kill(oldPid, 'SIGKILL');
|
|
162
|
+
await waitForPidExit(oldPid, 2000);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
try {
|
|
166
|
+
const oldPid = readDaemonPidFile();
|
|
167
|
+
if (oldPid && oldPid !== process.pid) {
|
|
168
|
+
try {
|
|
169
|
+
process.kill(oldPid, 0);
|
|
170
|
+
if (serviceManagedStart) {
|
|
171
|
+
await stopExistingDaemon(oldPid, 'managed start taking over from existing daemon');
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
console.log(`[${new Date().toISOString()}] daemon already running (PID ${oldPid}), skipping duplicate start`);
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Stale pid; continue booting and overwrite it below.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* noop */
|
|
185
|
+
}
|
|
186
|
+
const orphanPids = isIsolatedDaemonE2eInstance(process.env, SHENNIAN_DIR)
|
|
187
|
+
? []
|
|
188
|
+
: findRunningDaemonProcessIds(process.pid);
|
|
189
|
+
if (orphanPids.length > 0) {
|
|
190
|
+
if (serviceManagedStart) {
|
|
191
|
+
for (const orphanPid of orphanPids) {
|
|
192
|
+
try {
|
|
193
|
+
await stopExistingDaemon(orphanPid, 'managed start taking over from orphan daemon');
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Keep booting; the normal pid-file guard below still protects the managed daemon.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
console.log(`[${new Date().toISOString()}] daemon already running (PID ${orphanPids[0]}), skipping duplicate start`);
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const daemonInstanceId = createDaemonInstanceId();
|
|
206
|
+
writeDaemonPid(process.pid, daemonInstanceId, { version: cliVersion });
|
|
207
|
+
writeDaemonLauncher(process.pid, undefined, daemonInstanceId);
|
|
208
|
+
process.on('exit', () => {
|
|
209
|
+
clearDaemonPidIfOwner(process.pid, daemonInstanceId);
|
|
210
|
+
clearDaemonLauncher(daemonInstanceId);
|
|
211
|
+
});
|
|
212
|
+
// Crash detection: if we're recovering from a failed upgrade, rollback and exit
|
|
213
|
+
const didRollback = await handleStartupCrashCheck();
|
|
214
|
+
if (didRollback) {
|
|
215
|
+
console.log(`[${new Date().toISOString()}] Rolled back to previous version, restarting...`);
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
// No personal producer may open its store until the machine generation is
|
|
219
|
+
// fully migrated and its crash-safe marker is committed.
|
|
220
|
+
try {
|
|
221
|
+
ensurePersonalDataEpoch();
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
console.error(`[personal-sync] data epoch initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const config = loadConfig();
|
|
229
|
+
const serverUrl = opts.api ?? process.env.SHENNIAN_DESKTOP_SERVER_URL ?? config.serverUrl ?? SERVERS.cn.url;
|
|
230
|
+
const currentCliVersion = getCurrentVersion();
|
|
231
|
+
const detectedAgents = detectAgents();
|
|
232
|
+
const agentList = detectedAgents.map((a) => a.type);
|
|
233
|
+
const cachedAgentInfos = getCachedAgentInfos(detectedAgents);
|
|
234
|
+
const roomSessionRegistry = new RoomSessionRegistry();
|
|
235
|
+
const roomHostSessionObservations = new RoomHostSessionObservationStore();
|
|
236
|
+
const managedRoomContexts = new ManagedRoomContextAuthority(roomSessionRegistry);
|
|
237
|
+
const roomBindingClient = new HttpRoomBindingClient(serverUrl);
|
|
238
|
+
const roomAttachmentCache = new RoomAttachmentCache({ actionClient: roomBindingClient });
|
|
239
|
+
const roomAttachmentUploader = new RoomAttachmentUploader({ actionClient: roomBindingClient });
|
|
240
|
+
let roomActivationService = null;
|
|
241
|
+
const managedRoomExecutor = createManagedRoomExecutor({
|
|
242
|
+
authority: managedRoomContexts,
|
|
243
|
+
actionClient: roomBindingClient,
|
|
244
|
+
attachmentCache: roomAttachmentCache,
|
|
245
|
+
attachmentUploader: roomAttachmentUploader,
|
|
246
|
+
observations: roomHostSessionObservations,
|
|
247
|
+
getMachineCredentials: () => ({ machineToken: loadConfig().machineToken }),
|
|
248
|
+
});
|
|
249
|
+
const daemonIpc = await startDaemonIpcServer({
|
|
250
|
+
paths: resolveManagedRoomIpcPaths(),
|
|
251
|
+
handlers: {
|
|
252
|
+
'managed-room.execute': (params) => managedRoomExecutor(params),
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
let stoppingBeforeRelay = false;
|
|
256
|
+
const stopBeforeRelay = () => {
|
|
257
|
+
if (stoppingBeforeRelay)
|
|
258
|
+
return;
|
|
259
|
+
stoppingBeforeRelay = true;
|
|
260
|
+
void daemonIpc.close().finally(() => process.exit(0));
|
|
261
|
+
};
|
|
262
|
+
process.once('SIGINT', stopBeforeRelay);
|
|
263
|
+
process.once('SIGTERM', stopBeforeRelay);
|
|
264
|
+
if (!config.machineToken) {
|
|
265
|
+
console.error('Shennian CLI is not paired. Run "shennian pair --browser" or "shennian pair --token" first.');
|
|
266
|
+
await daemonIpc.close();
|
|
267
|
+
process.exitCode = 1;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
process.removeListener('SIGINT', stopBeforeRelay);
|
|
271
|
+
process.removeListener('SIGTERM', stopBeforeRelay);
|
|
272
|
+
const wsBase = httpToWs(serverUrl);
|
|
273
|
+
const wsUrl = `${wsBase}/relay/machine`;
|
|
274
|
+
const machineToken = config.machineToken;
|
|
275
|
+
if (!machineToken)
|
|
276
|
+
return;
|
|
277
|
+
try {
|
|
278
|
+
await synchronizeMachinePersonalAuthority({
|
|
279
|
+
serverUrl,
|
|
280
|
+
machineToken,
|
|
281
|
+
machineId: config.machineId,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
console.error(`[personal-sync] machine authority verification failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
286
|
+
await daemonIpc.close();
|
|
287
|
+
process.exitCode = 1;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (config.machineId) {
|
|
291
|
+
initCliLogReporter(serverUrl, config.machineId);
|
|
292
|
+
}
|
|
293
|
+
console.log(`[${new Date().toISOString()}] Connecting to ${wsUrl}... (v${currentCliVersion}) agents: ${agentList.join(',')}`);
|
|
294
|
+
reportLog({
|
|
295
|
+
level: 'info',
|
|
296
|
+
wsEvent: 'daemon.start',
|
|
297
|
+
metadata: { version: currentCliVersion, agents: agentList },
|
|
298
|
+
});
|
|
299
|
+
let nativeFusion = null;
|
|
300
|
+
let roomMirrorSync = null;
|
|
301
|
+
let deletionCatchupGeneration = 0;
|
|
302
|
+
const autoUpgradePolicy = normalizeAutoUpgradePolicy(config.autoUpgrade);
|
|
303
|
+
const client = new CliRelayClient({
|
|
304
|
+
serverUrl: wsUrl,
|
|
305
|
+
machineToken,
|
|
306
|
+
cliVersion: currentCliVersion,
|
|
307
|
+
agentList,
|
|
308
|
+
autoUpgradePolicy,
|
|
309
|
+
sessionSyncProtocolVersion: SESSION_SYNC_PROTOCOL_VERSION,
|
|
310
|
+
sessionSyncCapabilities: SESSION_SYNC_ADVERTISED_CAPABILITIES,
|
|
311
|
+
daemonInstallationId: getDaemonInstallationId(),
|
|
312
|
+
dataEpoch: CLI_SESSION_SYNC_DATA_EPOCH,
|
|
313
|
+
onConnected: () => {
|
|
314
|
+
console.log(`[${new Date().toISOString()}] ✓ Connected`);
|
|
315
|
+
reportLog({ level: 'info', wsEvent: 'daemon.connected' });
|
|
316
|
+
if (cachedAgentInfos.some((agent) => agent.models.length > 0)) {
|
|
317
|
+
client.sendEvent({
|
|
318
|
+
type: 'event',
|
|
319
|
+
event: 'machine.agents',
|
|
320
|
+
payload: {
|
|
321
|
+
agentList,
|
|
322
|
+
agents: cachedAgentInfos,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
void resolveAgentInfos(detectedAgents, {
|
|
327
|
+
serverUrl: config.serverUrl ?? serverUrl,
|
|
328
|
+
authToken: config.machineToken ?? config.accessToken,
|
|
329
|
+
})
|
|
330
|
+
.then((agents) => {
|
|
331
|
+
if (JSON.stringify(agents) === JSON.stringify(cachedAgentInfos))
|
|
332
|
+
return;
|
|
333
|
+
client.sendEvent({
|
|
334
|
+
type: 'event',
|
|
335
|
+
event: 'machine.agents',
|
|
336
|
+
payload: {
|
|
337
|
+
agentList,
|
|
338
|
+
agents,
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
})
|
|
342
|
+
.catch(() => { });
|
|
343
|
+
import('./upgrade/engine.js')
|
|
344
|
+
.then(({ readUpgradeAttempt, clearUpgradeAttempt }) => {
|
|
345
|
+
const attempt = readUpgradeAttempt();
|
|
346
|
+
clearUpgradeAttempt();
|
|
347
|
+
if (attempt) {
|
|
348
|
+
console.log(`[${new Date().toISOString()}] [upgrade] Reporting success: ${attempt.from} → ${attempt.to}`);
|
|
349
|
+
client.sendEvent({
|
|
350
|
+
type: 'event',
|
|
351
|
+
event: 'upgrade',
|
|
352
|
+
payload: {
|
|
353
|
+
state: 'success',
|
|
354
|
+
machineId: 'self',
|
|
355
|
+
from: attempt.from,
|
|
356
|
+
to: attempt.to,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
.catch(() => { });
|
|
362
|
+
upgradeScheduler.start();
|
|
363
|
+
roomMirrorSync?.start();
|
|
364
|
+
const catchupGeneration = ++deletionCatchupGeneration;
|
|
365
|
+
const catchUpBeforeDiscovery = async () => {
|
|
366
|
+
try {
|
|
367
|
+
await sessionManager.synchronizeSessionDeletions();
|
|
368
|
+
if (catchupGeneration === deletionCatchupGeneration &&
|
|
369
|
+
client.getState() === 'connected') {
|
|
370
|
+
nativeFusion?.handleConnected();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
// Discovery and dirty-index publication deliberately stay stopped:
|
|
375
|
+
// scanning before deletion catch-up can resurrect local history.
|
|
376
|
+
console.error('[session.deletions] startup catch-up failed', error);
|
|
377
|
+
if (catchupGeneration === deletionCatchupGeneration &&
|
|
378
|
+
client.getState() === 'connected') {
|
|
379
|
+
if (error instanceof DeletionSyncTerminalError)
|
|
380
|
+
return;
|
|
381
|
+
const delay = error instanceof DeletionSyncRetryWaitError
|
|
382
|
+
? Math.max(1_000, error.retryAfterMs)
|
|
383
|
+
: 5_000;
|
|
384
|
+
setTimeout(() => void catchUpBeforeDiscovery(), delay).unref?.();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
void catchUpBeforeDiscovery();
|
|
389
|
+
},
|
|
390
|
+
onDisconnected: (info) => {
|
|
391
|
+
deletionCatchupGeneration += 1;
|
|
392
|
+
roomMirrorSync?.stop();
|
|
393
|
+
console.log(`[${new Date().toISOString()}] ⚠ Disconnected, reconnecting... ${formatDisconnectInfo(info)}`);
|
|
394
|
+
reportLog({
|
|
395
|
+
level: 'warn',
|
|
396
|
+
wsEvent: 'daemon.disconnected',
|
|
397
|
+
metadata: {
|
|
398
|
+
code: info.code,
|
|
399
|
+
reason: info.reason,
|
|
400
|
+
error: info.error,
|
|
401
|
+
phase: info.phase,
|
|
402
|
+
trigger: info.trigger,
|
|
403
|
+
reconnectAttempt: info.reconnectAttempt,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
},
|
|
407
|
+
onReq: (req) => {
|
|
408
|
+
console.log(`[${new Date().toISOString()}] [req] ${req.method}`);
|
|
409
|
+
reportLog({
|
|
410
|
+
level: 'info',
|
|
411
|
+
type: 'ws',
|
|
412
|
+
wsEvent: req.method,
|
|
413
|
+
wsDirection: 'in',
|
|
414
|
+
traceId: req.traceId,
|
|
415
|
+
});
|
|
416
|
+
void sessionManager.handleReq(req);
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
nativeFusion = isNativeFusionEnabled() ? new NativeSessionFusionService(client) : null;
|
|
420
|
+
roomMirrorSync = new RoomMirrorSyncService({
|
|
421
|
+
registry: roomSessionRegistry,
|
|
422
|
+
observations: roomHostSessionObservations,
|
|
423
|
+
actionClient: roomBindingClient,
|
|
424
|
+
getMachineCredentials: () => ({ machineToken: loadConfig().machineToken }),
|
|
425
|
+
onMessages: async (roomId, messages, registrations, context) => {
|
|
426
|
+
await roomActivationService?.processMessages(roomId, messages, registrations, context);
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
const upgradeScheduler = new AutoUpgradeScheduler({
|
|
430
|
+
currentVersion: currentCliVersion,
|
|
431
|
+
machineId: config.machineId ?? 'unknown-machine',
|
|
432
|
+
serverUrl: config.serverUrl ?? serverUrl,
|
|
433
|
+
getIdleState: () => sessionManager.getUpgradeIdleState(),
|
|
434
|
+
startUpgrade: async (targetVersion) => {
|
|
435
|
+
const { handleUpgradeStart } = await import('./commands/upgrade.js');
|
|
436
|
+
await handleUpgradeStart(client, 'auto-upgrade', targetVersion, {
|
|
437
|
+
currentVersion: currentCliVersion,
|
|
438
|
+
});
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
upgradeScheduler.setPolicy(autoUpgradePolicy);
|
|
442
|
+
const sessionManager = new SessionManager(client, nativeFusion, currentCliVersion, upgradeScheduler, new TrustedManagedRoomBinder({
|
|
443
|
+
registry: roomSessionRegistry,
|
|
444
|
+
observations: roomHostSessionObservations,
|
|
445
|
+
bindingClient: roomBindingClient,
|
|
446
|
+
getMachineCredentials: () => {
|
|
447
|
+
const current = loadConfig();
|
|
448
|
+
return { machineId: current.machineId, machineToken: current.machineToken };
|
|
449
|
+
},
|
|
450
|
+
hostProcessIdentity: () => `daemon:${daemonInstanceId}`,
|
|
451
|
+
}), {
|
|
452
|
+
prepare: () => prepareRemoteAccessPause(),
|
|
453
|
+
isDisabled: () => isRemoteAccessDisabled(),
|
|
454
|
+
shutdownAfterResponse: () => {
|
|
455
|
+
setTimeout(() => {
|
|
456
|
+
stopPreparedRemoteAccessService();
|
|
457
|
+
process.kill(process.pid, 'SIGTERM');
|
|
458
|
+
}, 100).unref?.();
|
|
459
|
+
},
|
|
460
|
+
}, (token) => managedRoomContexts.revokeToken(token), managedRoomExecutor);
|
|
461
|
+
roomActivationService = new RoomActivationService({
|
|
462
|
+
registry: roomSessionRegistry,
|
|
463
|
+
observations: roomHostSessionObservations,
|
|
464
|
+
enqueueConversationBatch: (inputs) => sessionManager.enqueueManagedRoomBatch(inputs),
|
|
465
|
+
loadContext: async (registration) => {
|
|
466
|
+
const machineToken = loadConfig().machineToken?.trim();
|
|
467
|
+
const binding = registration.binding;
|
|
468
|
+
if (!machineToken || !binding)
|
|
469
|
+
throw new Error('managed_room_context_unavailable');
|
|
470
|
+
const remote = await roomBindingClient.status({
|
|
471
|
+
machineToken,
|
|
472
|
+
roomId: binding.roomId,
|
|
473
|
+
bindingId: binding.bindingId,
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
roomName: remote.room.name,
|
|
477
|
+
memberCount: remote.room.memberCount,
|
|
478
|
+
members: remote.room.members,
|
|
479
|
+
agentsByIdentityId: remote.agent ? { [remote.agentIdentityId]: remote.agent } : {},
|
|
480
|
+
agentSystemPromptsByIdentityId: remote.agent
|
|
481
|
+
? { [remote.agentIdentityId]: remote.agent.systemPrompt ?? '' }
|
|
482
|
+
: {},
|
|
483
|
+
};
|
|
484
|
+
},
|
|
485
|
+
issueManagedRoomContext: (registryKey, activationCauseMessageId) => {
|
|
486
|
+
const machineToken = loadConfig().machineToken;
|
|
487
|
+
if (!machineToken)
|
|
488
|
+
throw new Error('machine_not_authorized');
|
|
489
|
+
return managedRoomContexts.issue({
|
|
490
|
+
registryKey,
|
|
491
|
+
machineCredential: machineToken,
|
|
492
|
+
activationCauseMessageId,
|
|
493
|
+
});
|
|
494
|
+
},
|
|
495
|
+
audit: new RoomActivationAudit(),
|
|
496
|
+
});
|
|
497
|
+
sessionManager.setManagedRoomTurnPreparer((contexts) => roomActivationService.prepareConversationTurn(contexts));
|
|
498
|
+
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
499
|
+
writeDaemonPid(process.pid, daemonInstanceId, { version: currentCliVersion });
|
|
500
|
+
writeDaemonLauncher(process.pid, undefined, daemonInstanceId);
|
|
501
|
+
client.connect();
|
|
502
|
+
process.stdin.resume();
|
|
503
|
+
let shuttingDown = false;
|
|
504
|
+
const shutdown = async () => {
|
|
505
|
+
if (shuttingDown)
|
|
506
|
+
return;
|
|
507
|
+
shuttingDown = true;
|
|
508
|
+
console.log(chalk.gray('\nDisconnecting...'));
|
|
509
|
+
reportLog({ level: 'info', wsEvent: 'daemon.stop' });
|
|
510
|
+
clearInterval(logRetentionTimer);
|
|
511
|
+
try {
|
|
512
|
+
await sessionManager.cleanup();
|
|
513
|
+
upgradeScheduler.stop();
|
|
514
|
+
nativeFusion?.stop();
|
|
515
|
+
roomMirrorSync?.stop();
|
|
516
|
+
client.disconnect();
|
|
517
|
+
await daemonIpc.close();
|
|
518
|
+
clearDaemonPidIfOwner(process.pid, daemonInstanceId);
|
|
519
|
+
clearDaemonLauncher(daemonInstanceId);
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
process.exit(0);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
process.on('SIGINT', () => void shutdown());
|
|
526
|
+
process.on('SIGTERM', () => void shutdown());
|
|
527
|
+
});
|
|
528
|
+
program
|
|
529
|
+
.command('workbuddy-session-hook', { hidden: true })
|
|
530
|
+
.description('(retired) WorkBuddy Room Hook')
|
|
531
|
+
.allowUnknownOption(true)
|
|
532
|
+
.action(() => writeRetiredIntegrationError());
|
|
533
|
+
program
|
|
534
|
+
.command('room-session-hook', { hidden: true })
|
|
535
|
+
.description('(internal) Authorize the official Room tool for one trusted Agent Session')
|
|
536
|
+
.argument('<runtime>', 'Trusted runtime adapter')
|
|
537
|
+
.allowUnknownOption(true)
|
|
538
|
+
.action(() => writeRetiredIntegrationError());
|
|
539
|
+
program
|
|
540
|
+
.command('room-tool-server', { hidden: true })
|
|
541
|
+
.description('(internal) Run the official Shennian Room MCP tool')
|
|
542
|
+
.allowUnknownOption(true)
|
|
543
|
+
.action(() => writeRetiredIntegrationError());
|
|
544
|
+
// config: view and modify settings
|
|
545
|
+
const configCmd = program.command('config').description('View or modify configuration');
|
|
546
|
+
configCmd
|
|
547
|
+
.command('show', { isDefault: true })
|
|
548
|
+
.description('Show current configuration')
|
|
549
|
+
.action(() => {
|
|
550
|
+
console.log(`Config file: ${getConfigPath()}`);
|
|
551
|
+
const config = loadConfig();
|
|
552
|
+
const display = sanitizeCliLogValue(config, collectKnownLogSecrets(process.env, config));
|
|
553
|
+
if (config.serverUrl) {
|
|
554
|
+
const region = urlToRegion(config.serverUrl);
|
|
555
|
+
console.log(`Region: ${region} (${SERVERS[region].label})`);
|
|
556
|
+
}
|
|
557
|
+
console.log(JSON.stringify(display, null, 2));
|
|
558
|
+
});
|
|
559
|
+
configCmd
|
|
560
|
+
.command('set')
|
|
561
|
+
.description('Update a config value')
|
|
562
|
+
.argument('<key>', 'Config key (e.g. "server")')
|
|
563
|
+
.argument('<value>', 'Config value (e.g. "cn" or "global")')
|
|
564
|
+
.action((key, value) => {
|
|
565
|
+
const config = loadConfig();
|
|
566
|
+
if (key === 'server') {
|
|
567
|
+
if (value !== 'cn' && value !== 'global') {
|
|
568
|
+
console.error(chalk.red('✗ Value must be "cn" or "global"'));
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
const region = value;
|
|
572
|
+
config.serverUrl = regionToUrl(region);
|
|
573
|
+
saveConfig(config);
|
|
574
|
+
console.log(chalk.green(`✓ Server set to ${SERVERS[region].label}`));
|
|
575
|
+
console.log(chalk.yellow(' Restart the background service for this to take effect: shennian stop && shennian start'));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
console.error(chalk.red(`✗ Unknown config key: ${key}. Supported: server`));
|
|
579
|
+
process.exit(1);
|
|
580
|
+
});
|
|
581
|
+
registerPairCommand(program);
|
|
582
|
+
registerDaemonCommand(program);
|
|
583
|
+
registerRuntimeCommand(program);
|
|
584
|
+
registerManagerCommand(program);
|
|
585
|
+
registerUpgradeCommand(program);
|
|
586
|
+
registerToolsCommand(program);
|
|
587
|
+
program.parse();
|