larkin 0.4.12 → 0.4.14
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/agent/agent-state-store.cjs +237 -0
- package/dist/agent/agent-state-store.mjs +238 -1
- package/dist/agent/context-prompt.cjs +2 -1
- package/dist/agent/context-prompt.mjs +2 -1
- package/dist/agent/host-reminder-orchestrator.cjs +134 -7
- package/dist/agent/host-reminder-orchestrator.mjs +134 -7
- package/dist/agent/inbox-projection.cjs +5 -0
- package/dist/agent/inbox-projection.mjs +4 -0
- package/dist/agent/reminder-delivery-audit.cjs +86 -0
- package/dist/agent/reminder-delivery-audit.mjs +50 -0
- package/dist/agent/reminder-routes.cjs +83 -3
- package/dist/agent/reminder-routes.mjs +83 -3
- package/dist/agent/reminder-store.cjs +5 -2
- package/dist/agent/reminder-store.mjs +5 -2
- package/dist/agent/transport-business-context.cjs +26 -2
- package/dist/agent/transport-business-context.mjs +26 -2
- package/dist/app/agent-cli.mjs +33 -2
- package/dist/app/agent-lark-cli-workspace.cjs +3 -2
- package/dist/app/agent-lark-cli-workspace.mjs +3 -2
- package/dist/app/lark-cli.mjs +79 -11
- package/dist/app/local-control.mjs +64 -6
- package/dist/app/run.mjs +64 -24
- package/dist/app/runtime-agent-config.mjs +58 -5
- package/dist/app/runtime-process.mjs +6 -1
- package/dist/feishu/host-business-state.cjs +35 -14
- package/dist/feishu/host-business-state.mjs +35 -14
- package/dist/feishu/interaction-orchestrator.cjs +8 -1
- package/dist/feishu/interaction-orchestrator.mjs +8 -1
- package/dist/feishu/outbound-transport.cjs +37 -3
- package/dist/feishu/outbound-transport.mjs +37 -3
- package/dist/runtime/runtime-host.cjs +37 -0
- package/dist/runtime/runtime-host.mjs +37 -0
- package/package.json +1 -1
|
@@ -462,11 +462,15 @@ export function removeControlAuthority(larkinHome, expectedToken) {
|
|
|
462
462
|
throw error;
|
|
463
463
|
}
|
|
464
464
|
}
|
|
465
|
-
export function createSupervisorControlServer({ larkinHome, authorityToken, ensureDashboard, }) {
|
|
465
|
+
export function createSupervisorControlServer({ larkinHome, authorityToken, ensureDashboard, onAgentUpserted, }) {
|
|
466
466
|
let socket = "";
|
|
467
467
|
let socketRoot = "";
|
|
468
468
|
let socketIdentity = null;
|
|
469
469
|
const completed = new Map();
|
|
470
|
+
const operationConflict = (operationId, agentId) => ({
|
|
471
|
+
ok: false, operationId, ...(agentId ? { agentId } : {}), code: "operation_conflict",
|
|
472
|
+
error: "operationId 已绑定其他 Agent 或操作",
|
|
473
|
+
});
|
|
470
474
|
let server = null;
|
|
471
475
|
return {
|
|
472
476
|
async start() {
|
|
@@ -490,9 +494,15 @@ export function createSupervisorControlServer({ larkinHome, authorityToken, ensu
|
|
|
490
494
|
let request;
|
|
491
495
|
try {
|
|
492
496
|
request = JSON.parse(line);
|
|
493
|
-
|
|
497
|
+
const isDashboardRecovery = request.operation === "ensure-dashboard";
|
|
498
|
+
const isAgentUpsert = request.operation === "agent-upserted";
|
|
499
|
+
const allowedKeys = isDashboardRecovery
|
|
500
|
+
? ["operationId", "operation", "authorization"]
|
|
501
|
+
: isAgentUpsert ? ["operationId", "operation", "authorization", "agentId"] : [];
|
|
502
|
+
if (!OPERATION_ID.test(String(request.operationId || "")) || (!isDashboardRecovery && !isAgentUpsert)
|
|
494
503
|
|| !AUTHORIZATION.test(String(request.authorization || ""))
|
|
495
|
-
||
|
|
504
|
+
|| (isAgentUpsert && !AGENT_ID.test(String(request.agentId || "")))
|
|
505
|
+
|| Object.keys(request).some((key) => !allowedKeys.includes(key))) {
|
|
496
506
|
throw new Error("invalid supervisor control request");
|
|
497
507
|
}
|
|
498
508
|
const authority = assertSupervisorAuthority(larkinHome, authorityToken);
|
|
@@ -505,17 +515,28 @@ export function createSupervisorControlServer({ larkinHome, authorityToken, ensu
|
|
|
505
515
|
}
|
|
506
516
|
const replay = completed.get(request.operationId);
|
|
507
517
|
if (replay) {
|
|
508
|
-
|
|
518
|
+
const sameRequest = replay.operation === request.operation
|
|
519
|
+
&& replay.agentId === request.agentId;
|
|
520
|
+
const response = sameRequest ? replay.response : operationConflict(request.operationId, request.agentId);
|
|
521
|
+
connection.end(`${JSON.stringify(response)}\n`);
|
|
509
522
|
return;
|
|
510
523
|
}
|
|
511
524
|
let response;
|
|
512
525
|
try {
|
|
513
|
-
|
|
526
|
+
if (request.operation === "agent-upserted") {
|
|
527
|
+
await onAgentUpserted?.(request.agentId);
|
|
528
|
+
response = { ok: true, operationId: request.operationId, state: "agent-recorded" };
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
response = { ok: true, operationId: request.operationId, state: await ensureDashboard() };
|
|
532
|
+
}
|
|
514
533
|
}
|
|
515
534
|
catch (error) {
|
|
516
535
|
response = { ok: false, operationId: request.operationId, error: error instanceof Error ? error.message : String(error) };
|
|
517
536
|
}
|
|
518
|
-
completed.set(request.operationId,
|
|
537
|
+
completed.set(request.operationId, {
|
|
538
|
+
operation: request.operation, agentId: request.agentId, response,
|
|
539
|
+
});
|
|
519
540
|
while (completed.size > 256)
|
|
520
541
|
completed.delete(completed.keys().next().value);
|
|
521
542
|
connection.end(`${JSON.stringify(response)}\n`);
|
|
@@ -812,6 +833,43 @@ async function sendSupervisorRecovery(larkinHome, operationId, timeoutMs) {
|
|
|
812
833
|
});
|
|
813
834
|
});
|
|
814
835
|
}
|
|
836
|
+
async function sendSupervisorAgentUpsert(larkinHome, agentId, operationId, timeoutMs) {
|
|
837
|
+
const authority = assertSupervisorAuthority(larkinHome);
|
|
838
|
+
assertSecureSocketDirectory(authority.socketRoot);
|
|
839
|
+
const socket = authority.supervisorSocketPath;
|
|
840
|
+
const stat = fs.lstatSync(socket);
|
|
841
|
+
if (!stat.isSocket() || stat.isSymbolicLink()
|
|
842
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
843
|
+
|| !notGroupOrWorldAccessible(stat))
|
|
844
|
+
throw new Error("supervisor control socket 不安全");
|
|
845
|
+
return await new Promise((resolve, reject) => {
|
|
846
|
+
const client = net.createConnection(socket);
|
|
847
|
+
const timer = setTimeout(() => { client.destroy(); reject(new Error("supervisor agent upsert control timeout")); }, timeoutMs);
|
|
848
|
+
let input = "";
|
|
849
|
+
client.setEncoding("utf8");
|
|
850
|
+
client.once("error", (error) => { clearTimeout(timer); reject(error); });
|
|
851
|
+
client.once("connect", () => client.write(`${JSON.stringify({
|
|
852
|
+
operationId, operation: "agent-upserted", agentId, authorization: authority.token,
|
|
853
|
+
})}\n`));
|
|
854
|
+
client.on("data", (chunk) => {
|
|
855
|
+
input += chunk;
|
|
856
|
+
const newline = input.indexOf("\n");
|
|
857
|
+
if (newline < 0)
|
|
858
|
+
return;
|
|
859
|
+
clearTimeout(timer);
|
|
860
|
+
client.end();
|
|
861
|
+
try {
|
|
862
|
+
resolve(JSON.parse(input.slice(0, newline)));
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
reject(error);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
export async function requestSupervisorAgentUpsert({ larkinHome, agentId, operationId = crypto.randomUUID(), timeoutMs = 30_000, }) {
|
|
871
|
+
return sendSupervisorAgentUpsert(larkinHome, agentId, operationId, timeoutMs);
|
|
872
|
+
}
|
|
815
873
|
export async function requestDashboardRecovery({ larkinHome, operationId = crypto.randomUUID(), timeoutMs = 30_000, }) {
|
|
816
874
|
const deadline = Date.now() + timeoutMs;
|
|
817
875
|
let lastError;
|
package/dist/app/run.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import * as larkinConfig from "../platform/config.mjs";
|
|
9
9
|
import { currentProcessMetadata } from "../platform/process-inspect.cjs";
|
|
10
10
|
import { acquireProcessLock, readProcessState, terminateOwnedProcess, waitForProcessExit, } from "../platform/process-state.mjs";
|
|
11
|
-
import { hydrateRuntimeAgent, syncAgentProfile } from "./runtime-agent-config.mjs";
|
|
11
|
+
import { hydrateRuntimeAgent, syncAgentProfile, validateAgentProfile } from "./runtime-agent-config.mjs";
|
|
12
12
|
import { cleanupStaleAgentControlSocket, createSupervisorControlServer, initializeControlAuthority, removeControlAuthority, requestDashboardRecovery, } from "./local-control.mjs";
|
|
13
13
|
import { internalCommandSpec, processCommandToken } from "./internal-command.mjs";
|
|
14
14
|
import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
|
|
@@ -80,6 +80,9 @@ export async function main() {
|
|
|
80
80
|
catch (error) {
|
|
81
81
|
return die(error.message);
|
|
82
82
|
}
|
|
83
|
+
// Capture this supervisor's startup membership before setup can persist a new
|
|
84
|
+
// Agent. Unqualified selection must not expand from later durable config.
|
|
85
|
+
const initialNames = new Set(names);
|
|
83
86
|
const before = readProcessState(configDir);
|
|
84
87
|
if (before.supervisor.state === "owned") {
|
|
85
88
|
if (before.dashboard.state !== "owned") {
|
|
@@ -124,11 +127,16 @@ export async function main() {
|
|
|
124
127
|
die("无法取得 supervisor start identity");
|
|
125
128
|
}
|
|
126
129
|
const controlToken = initializeControlAuthority(configDir, { pid: process.pid, processStartToken: supervisorStartToken });
|
|
130
|
+
// This supervisor's successful hot-attaches are the only membership additions
|
|
131
|
+
// that survive a daemon crash. Durable config apply state describes freshness,
|
|
132
|
+
// not which Agents this explicitly selected supervisor owns.
|
|
133
|
+
const hotAttachedNames = new Set();
|
|
127
134
|
let ensureDashboardHandler = () => "starting";
|
|
128
135
|
const supervisorControl = createSupervisorControlServer({
|
|
129
136
|
larkinHome: configDir,
|
|
130
137
|
authorityToken: controlToken,
|
|
131
138
|
ensureDashboard: () => ensureDashboardHandler(),
|
|
139
|
+
onAgentUpserted: (agentId) => { hotAttachedNames.add(agentId); },
|
|
132
140
|
});
|
|
133
141
|
try {
|
|
134
142
|
await supervisorControl.start();
|
|
@@ -141,16 +149,53 @@ export async function main() {
|
|
|
141
149
|
catch { /* best effort */ }
|
|
142
150
|
die(`supervisor control 启动失败:${error instanceof Error ? error.message : String(error)}`);
|
|
143
151
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
let agents = [];
|
|
153
|
+
const serverId = config.serverId;
|
|
154
|
+
if (!serverId)
|
|
155
|
+
die("配置缺少 serverId");
|
|
156
|
+
const runtimeEnv = {
|
|
157
|
+
...process.env,
|
|
158
|
+
LARKIN_HOME: configDir,
|
|
159
|
+
LARKIN_CONFIG_DIR: configDir,
|
|
160
|
+
LARKIN_AGENT_TRANSPORT_MODULE: path.join(HERE, "..", "agent", "agent-transport.cjs"),
|
|
161
|
+
LARKIN_SERVER_ID: String(serverId),
|
|
162
|
+
LARKIN_AGENTS_CONFIG: "[]",
|
|
163
|
+
LARKIN_CONTROL_AUTHORIZATION: controlToken,
|
|
164
|
+
};
|
|
165
|
+
if (argv.includes("--dry-run"))
|
|
166
|
+
runtimeEnv.LARKIN_FEISHU_DRYRUN = "1";
|
|
167
|
+
// Re-read durable config for every daemon launch instead of reusing the startup
|
|
168
|
+
// snapshot: a hot attach updates the child and durable config, but cannot mutate
|
|
169
|
+
// this supervisor's old closure. Membership remains initialNames plus successful
|
|
170
|
+
// hot attaches recorded during this supervisor lifetime.
|
|
171
|
+
const refreshDaemonAgents = (reuseExistingProfiles) => {
|
|
172
|
+
const latest = larkinConfig.loadConfig(process.env);
|
|
173
|
+
if (latest.configDir !== configDir)
|
|
174
|
+
throw new Error("配置目录在 supervisor 运行期间发生变化");
|
|
175
|
+
if (!latest.config.serverId || !Object.keys(latest.config.agents).length)
|
|
176
|
+
throw new Error("配置缺少 serverId/agents");
|
|
177
|
+
const latestNames = [...new Set([...initialNames, ...hotAttachedNames])];
|
|
178
|
+
const refreshed = [];
|
|
179
|
+
for (const name of latestNames) {
|
|
180
|
+
const stored = latest.config.agents[name];
|
|
148
181
|
if (!stored)
|
|
149
182
|
throw new Error(`Agent ${name} 不存在`);
|
|
150
183
|
const agent = hydrateRuntimeAgent(configDir, stored);
|
|
151
|
-
|
|
152
|
-
|
|
184
|
+
if (reuseExistingProfiles)
|
|
185
|
+
validateAgentProfile(agent);
|
|
186
|
+
else
|
|
187
|
+
syncAgentProfile(agent, { ...process.env, LARKIN_CONFIG_DIR: configDir });
|
|
188
|
+
refreshed.push(agent);
|
|
153
189
|
}
|
|
190
|
+
agents = refreshed;
|
|
191
|
+
names = latestNames;
|
|
192
|
+
runtimeEnv.LARKIN_SERVER_ID = String(latest.config.serverId);
|
|
193
|
+
runtimeEnv.LARKIN_AGENTS_CONFIG = JSON.stringify(agents);
|
|
194
|
+
for (const agent of agents)
|
|
195
|
+
traceProcessBoundary(runtimeEnv, "supervisor:daemon-env-prepared", { configDir, agentId: agent.agentId, targetDir: path.join(configDir, "providers", "pi", agent.agentId) });
|
|
196
|
+
};
|
|
197
|
+
try {
|
|
198
|
+
refreshDaemonAgents(false);
|
|
154
199
|
}
|
|
155
200
|
catch (error) {
|
|
156
201
|
await supervisorControl.close().catch(() => { });
|
|
@@ -183,22 +228,6 @@ export async function main() {
|
|
|
183
228
|
launchLock.release();
|
|
184
229
|
die(`dashboard PID ${before.dashboard.pid} 身份无法确认;拒绝替换`);
|
|
185
230
|
}
|
|
186
|
-
const serverId = config.serverId;
|
|
187
|
-
if (!serverId)
|
|
188
|
-
die("配置缺少 serverId");
|
|
189
|
-
const runtimeEnv = {
|
|
190
|
-
...process.env,
|
|
191
|
-
LARKIN_HOME: configDir,
|
|
192
|
-
LARKIN_CONFIG_DIR: configDir,
|
|
193
|
-
LARKIN_AGENT_TRANSPORT_MODULE: path.join(HERE, "..", "agent", "agent-transport.cjs"),
|
|
194
|
-
LARKIN_SERVER_ID: String(serverId),
|
|
195
|
-
LARKIN_AGENTS_CONFIG: JSON.stringify(agents),
|
|
196
|
-
LARKIN_CONTROL_AUTHORIZATION: controlToken,
|
|
197
|
-
};
|
|
198
|
-
if (argv.includes("--dry-run"))
|
|
199
|
-
runtimeEnv.LARKIN_FEISHU_DRYRUN = "1";
|
|
200
|
-
for (const agent of agents)
|
|
201
|
-
traceProcessBoundary(runtimeEnv, "supervisor:daemon-env-prepared", { configDir, agentId: agent.agentId, targetDir: path.join(configDir, "providers", "pi", agent.agentId) });
|
|
202
231
|
let stopping = false;
|
|
203
232
|
let dashboard = null;
|
|
204
233
|
// Set when the daemon-restart path stops the dashboard on purpose (port
|
|
@@ -222,7 +251,19 @@ export async function main() {
|
|
|
222
251
|
// exiting on transient failures (e.g. network/proxy ECONNRESET during Feishu
|
|
223
252
|
// auth). Only when the budget is exhausted does the supervisor exit so the
|
|
224
253
|
// external process manager (launchd) takes over as the final respawn layer.
|
|
254
|
+
let initialRefreshPending = true;
|
|
225
255
|
const launchDaemon = () => {
|
|
256
|
+
if (initialRefreshPending)
|
|
257
|
+
initialRefreshPending = false;
|
|
258
|
+
else
|
|
259
|
+
try {
|
|
260
|
+
refreshDaemonAgents(true);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
console.error(`✗ daemon 启动前重新加载配置失败:${error instanceof Error ? error.message : String(error)}`);
|
|
264
|
+
resolveDaemonFinal?.({ code: null, signal: null });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
226
267
|
daemon = spawn(daemonSpec.command, daemonSpec.args, { env: runtimeEnv, stdio: "inherit" });
|
|
227
268
|
for (const agent of agents)
|
|
228
269
|
traceProcessBoundary(runtimeEnv, "supervisor:daemon-spawned", { configDir, agentId: agent.agentId, targetDir: path.join(configDir, "providers", "pi", agent.agentId), childPid: daemon.pid ?? null });
|
|
@@ -257,7 +298,6 @@ export async function main() {
|
|
|
257
298
|
const oldDashboard = dashboard;
|
|
258
299
|
if (oldDashboard && oldDashboard.exitCode === null && oldDashboard.signalCode === null) {
|
|
259
300
|
intentionalDashboardExit = oldDashboard;
|
|
260
|
-
dashboard = null;
|
|
261
301
|
oldDashboard.kill("SIGTERM");
|
|
262
302
|
// If the old dashboard ignores SIGTERM (observed in production holding
|
|
263
303
|
// the dashboard port for minutes), escalate so the port is released
|
|
@@ -17,6 +17,13 @@ function assertSecureProfileDirectory(directory) {
|
|
|
17
17
|
|| !exactMode(stat, 0o700))
|
|
18
18
|
throw new Error("lark-cli profile 目录不安全");
|
|
19
19
|
}
|
|
20
|
+
function assertValidProfileDirectory(directory) {
|
|
21
|
+
const stat = fs.lstatSync(directory);
|
|
22
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
23
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
24
|
+
|| (!exactMode(stat, 0o700) && !exactMode(stat, 0o500)))
|
|
25
|
+
throw new Error("lark-cli profile 目录不安全");
|
|
26
|
+
}
|
|
20
27
|
function captureProfileSnapshot(file) {
|
|
21
28
|
let fd = null;
|
|
22
29
|
try {
|
|
@@ -233,22 +240,44 @@ function runOfficialLarkCliAsync(command, args, env, dependencies) {
|
|
|
233
240
|
});
|
|
234
241
|
});
|
|
235
242
|
}
|
|
243
|
+
function expectedRuntimeCommandShim() {
|
|
244
|
+
const standalone = process.env.LARKIN_STANDALONE === "1";
|
|
245
|
+
const binaryEntry = fileURLToPath(new URL("./binary-entry.mjs", import.meta.url));
|
|
246
|
+
const argumentsPrefix = standalone ? [] : [binaryEntry];
|
|
247
|
+
const command = [process.execPath, ...argumentsPrefix].map(shellQuote).join(" ");
|
|
248
|
+
return `#!/bin/sh\nexec ${command} "$@"\n`;
|
|
249
|
+
}
|
|
236
250
|
export function installRuntimeCommandShims(agent) {
|
|
237
251
|
const stateDir = path.resolve(agent.stateDir);
|
|
238
252
|
const commandDir = path.join(stateDir, "runtime-bin");
|
|
239
253
|
assertSecureRuntimeCommandDirectory(commandDir);
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
for (const [name, argumentsPrefix] of [["larkin", standalone ? [] : [binaryEntry]]]) {
|
|
254
|
+
const fileContents = expectedRuntimeCommandShim();
|
|
255
|
+
for (const name of ["larkin"]) {
|
|
243
256
|
const file = path.join(commandDir, name);
|
|
244
257
|
const temporary = path.join(commandDir, `.${name}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
245
|
-
|
|
246
|
-
fs.writeFileSync(temporary, `#!/bin/sh\nexec ${command} "$@"\n`, { mode: 0o700, flag: "wx" });
|
|
258
|
+
fs.writeFileSync(temporary, fileContents, { mode: 0o700, flag: "wx" });
|
|
247
259
|
fs.renameSync(temporary, file);
|
|
248
260
|
fs.chmodSync(file, 0o700);
|
|
249
261
|
}
|
|
250
262
|
return commandDir;
|
|
251
263
|
}
|
|
264
|
+
function validateRuntimeCommandShims(agent) {
|
|
265
|
+
const commandDir = path.join(path.resolve(agent.stateDir), "runtime-bin");
|
|
266
|
+
const directory = fs.lstatSync(commandDir);
|
|
267
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()
|
|
268
|
+
|| (typeof process.getuid === "function" && directory.uid !== process.getuid())
|
|
269
|
+
|| (!exactMode(directory, 0o700) && !exactMode(directory, 0o500)))
|
|
270
|
+
throw new Error("Runtime command shim 目录不安全");
|
|
271
|
+
const file = path.join(commandDir, "larkin");
|
|
272
|
+
const stat = fs.lstatSync(file);
|
|
273
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
274
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
275
|
+
|| (!exactMode(stat, 0o700) && !exactMode(stat, 0o500)))
|
|
276
|
+
throw new Error("Runtime command shim 不安全");
|
|
277
|
+
if (fs.readFileSync(file, "utf8") !== expectedRuntimeCommandShim()) {
|
|
278
|
+
throw new Error("Runtime command shim 内容无效");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
252
281
|
export function sourceProjection(agent, env) {
|
|
253
282
|
const configDir = path.resolve(env.LARKIN_CONFIG_DIR || "");
|
|
254
283
|
if (!configDir)
|
|
@@ -299,7 +328,31 @@ function validateSourceProjection(file, agent) {
|
|
|
299
328
|
throw new Error(`Agent ${agent.agentId} lark-channel source projection tenant mismatch`);
|
|
300
329
|
}
|
|
301
330
|
}
|
|
331
|
+
export function validateAgentProfile(agent) {
|
|
332
|
+
// Validate the profile root before resolving any leaf paths. The supervisor's
|
|
333
|
+
// restart fast path must not follow a replaced symlink or unsafe root.
|
|
334
|
+
assertValidProfileDirectory(agent.larkConfigDir);
|
|
335
|
+
const sourceFile = larkChannelSourceConfigPath(agent);
|
|
336
|
+
const workspaceFile = larkChannelWorkspaceConfigPath(agent);
|
|
337
|
+
const source = captureProfileSnapshot(sourceFile);
|
|
338
|
+
if (!source)
|
|
339
|
+
throw new Error(`Agent ${agent.agentId} lark-channel source projection missing`);
|
|
340
|
+
validateSourceProjection(sourceFile, agent);
|
|
341
|
+
const workspace = captureProfileSnapshot(workspaceFile);
|
|
342
|
+
if (!workspace)
|
|
343
|
+
throw new Error(`Agent ${agent.agentId} lark-channel workspace config missing`);
|
|
344
|
+
validateExclusiveBotProfile(workspace, agent);
|
|
345
|
+
validateRuntimeCommandShims(agent);
|
|
346
|
+
assertAgentWorkspaceBound(agent);
|
|
347
|
+
}
|
|
302
348
|
export function syncAgentProfile(agent, env, dependencies = {}) {
|
|
349
|
+
if (!dependencies.forceRebind) {
|
|
350
|
+
try {
|
|
351
|
+
validateAgentProfile(agent);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
catch { /* absent or stale state requires exactly one bind */ }
|
|
355
|
+
}
|
|
303
356
|
const expected = path.join(path.resolve(env.LARKIN_CONFIG_DIR || ""), "state", "agents", agent.agentId, "lark-cli-config");
|
|
304
357
|
if (path.resolve(agent.larkConfigDir) !== expected)
|
|
305
358
|
throw new Error("lark-cli profile 路径不是 canonical contained 路径");
|
|
@@ -8,7 +8,7 @@ import { createRuntimeHost } from "../runtime/runtime-host.mjs";
|
|
|
8
8
|
import { createAgentStateStore } from "../agent/agent-state-store.mjs";
|
|
9
9
|
import { loadConfig, markConfigApplied, runtimeConfigSignature } from "../platform/config.mjs";
|
|
10
10
|
import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
|
|
11
|
-
import { createAgentControlServer } from "./local-control.mjs";
|
|
11
|
+
import { createAgentControlServer, requestSupervisorAgentUpsert } from "./local-control.mjs";
|
|
12
12
|
import { hydrateRuntimeAgent, syncAgentProfile } from "./runtime-agent-config.mjs";
|
|
13
13
|
import { loadTelemetryConfig } from "../platform/telemetry-config.mjs";
|
|
14
14
|
import { telemetrySingleton } from "../platform/telemetry-tracing.mjs";
|
|
@@ -104,6 +104,11 @@ export async function main(env = process.env, overrides = {}) {
|
|
|
104
104
|
// Only the selected profile is synchronized; active profiles and their directory
|
|
105
105
|
// are never quarantined or rebuilt during hot attach.
|
|
106
106
|
await hostShell.upsertAgent(agent);
|
|
107
|
+
const tracked = await requestSupervisorAgentUpsert({
|
|
108
|
+
larkinHome: env.LARKIN_HOME, agentId: agent.agentId, operationId: request.operationId,
|
|
109
|
+
});
|
|
110
|
+
if (!tracked.ok)
|
|
111
|
+
throw new Error(tracked.error || "supervisor 未记录 Agent 热挂载");
|
|
107
112
|
},
|
|
108
113
|
async resetSession(request) {
|
|
109
114
|
const result = await hostShell.resetSession(request.agentId, request.waitReadyMs);
|
|
@@ -316,31 +316,50 @@ class HostEnvelopeProjector {
|
|
|
316
316
|
}
|
|
317
317
|
createReminderEnvelope(agentId, reminder, overdueMs, repeatDescription) {
|
|
318
318
|
const seq = this.nextSequence(agentId);
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
319
|
+
const legacyChannel = typeof reminder.channel === "string" ? reminder.channel.trim() : "";
|
|
320
|
+
const legacyChatTarget = /^oc_[A-Za-z0-9_-]+$/.test(legacyChannel) ? `chat:${legacyChannel}` : null;
|
|
321
|
+
const deliveryTarget = typeof reminder.deliveryTarget === "string" && reminder.deliveryTarget
|
|
322
|
+
? reminder.deliveryTarget : legacyChatTarget;
|
|
323
|
+
const anchorMessageId = typeof reminder.deliveryAnchor === "string" && /^om_[A-Za-z0-9_-]+$/.test(reminder.deliveryAnchor)
|
|
324
|
+
? reminder.deliveryAnchor
|
|
325
|
+
: typeof reminder.msgRef === "string" && /^om_[A-Za-z0-9_-]+$/.test(reminder.msgRef)
|
|
326
|
+
? reminder.msgRef
|
|
327
|
+
: null;
|
|
328
|
+
const commentAnchorId = deliveryTarget?.startsWith("document-comment:") && typeof reminder.deliveryAnchor === "string"
|
|
329
|
+
&& /^doc_comment_[A-Za-z0-9_-]+$/.test(reminder.deliveryAnchor) ? reminder.deliveryAnchor : null;
|
|
330
|
+
const anchorId = anchorMessageId || commentAnchorId;
|
|
331
|
+
const chatId = deliveryTarget?.match(/^chat:(oc_[A-Za-z0-9_-]+)$/)?.[1] || null;
|
|
322
332
|
const lines = [
|
|
323
333
|
`[定时提醒触发] ${reminder.title}`,
|
|
324
334
|
`提醒ID: #${reminder.reminderId.slice(0, 8)}` + (reminder.repeat && repeatDescription
|
|
325
335
|
? ` 重复: ${repeatDescription}(下次已自动排在 ${reminder.fireAt})`
|
|
326
336
|
: " 类型: 一次性"),
|
|
327
337
|
overdueMs > 120_000 ? `注意: 原定时间已过 ${Math.round(overdueMs / 60_000)} 分钟(Runtime Host 离线期间错过,现补触发)` : null,
|
|
328
|
-
|
|
338
|
+
deliveryTarget ? `原始 deliveryTarget: ${deliveryTarget}`
|
|
339
|
+
: anchorId ? "这是升级前存量 user-facing reminder,优先回复其安全锚点;不得向标题中的任何人或第三方发送消息"
|
|
340
|
+
: "本条为 internal/no-delivery reminder,不得向标题中的任何人或第三方发送消息",
|
|
341
|
+
anchorId ? `锚定消息: ${anchorId}` : reminder.msgRef ? `历史锚点 ${String(reminder.msgRef)} 不是可用的 delivery anchor,不能用于回复` : null,
|
|
329
342
|
anchorMessageId
|
|
330
|
-
?
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
:
|
|
335
|
-
?
|
|
336
|
-
:
|
|
337
|
-
?
|
|
338
|
-
|
|
343
|
+
? [
|
|
344
|
+
`回复原会话: ${this.larkCommand(`im +messages-reply --message-id ${anchorMessageId}${deliveryTarget?.startsWith("thread:") ? " --reply-in-thread" : ""} ...`)}`,
|
|
345
|
+
chatId ? `若 ${anchorMessageId} 无法由当前 Inbox 解析(例如 interaction_* 卡片锚点或已淘汰的历史消息),改用聊天兜底发送: ${this.larkCommand(`im +messages-send --chat-id ${chatId} ...`)};仅使用这个持久化 chat_id,不得猜测收件人` : null,
|
|
346
|
+
].filter((line) => Boolean(line)).join("\n")
|
|
347
|
+
: commentAnchorId
|
|
348
|
+
? `回复原文档评论: ${this.agentCommand(`comment reply --message-id ${commentAnchorId} --text ...`)}`
|
|
349
|
+
: deliveryTarget
|
|
350
|
+
? chatId
|
|
351
|
+
? `发送到原始 target: ${deliveryTarget}: ${this.larkCommand(`im +messages-send --chat-id ${chatId} ...`)}(不得从提醒标题推断收件人)`
|
|
352
|
+
: `发送到原始 target: ${deliveryTarget}(不得从提醒标题推断收件人)`
|
|
353
|
+
: null,
|
|
339
354
|
`这是你之前用 ${this.agentCommand("reminder schedule")} 设置的提醒,请按标题执行相应动作。管理: ${this.agentCommand("reminder list")} / ${this.agentCommand("reminder snooze")} / ${this.agentCommand("reminder cancel")}`,
|
|
340
355
|
].filter((line) => Boolean(line));
|
|
341
356
|
const envelope = {
|
|
342
357
|
kind: "reminder",
|
|
343
|
-
|
|
358
|
+
// The random suffix keeps each firing's occurrence identity unique across
|
|
359
|
+
// Host restarts: the in-memory seq restarts at 1 and would otherwise let a
|
|
360
|
+
// recurring reminder reuse a prior occurrenceId in the delivery audit.
|
|
361
|
+
message_id: `rem_${reminder.reminderId.slice(0, 16)}_${seq}_${this.randomHex(6)}`,
|
|
362
|
+
reminderId: reminder.reminderId,
|
|
344
363
|
seq,
|
|
345
364
|
sender_name: "定时提醒",
|
|
346
365
|
sender_type: "system",
|
|
@@ -350,6 +369,8 @@ class HostEnvelopeProjector {
|
|
|
350
369
|
timestamp: this.now().toISOString(),
|
|
351
370
|
thread_id: null,
|
|
352
371
|
wake: true,
|
|
372
|
+
deliveryTarget,
|
|
373
|
+
deliveryAnchor: anchorId,
|
|
353
374
|
};
|
|
354
375
|
return { ...envelope, target: inbox_projection_js_1.RUNTIME_REMINDER_TARGET };
|
|
355
376
|
}
|
|
@@ -302,31 +302,50 @@ export class HostEnvelopeProjector {
|
|
|
302
302
|
}
|
|
303
303
|
createReminderEnvelope(agentId, reminder, overdueMs, repeatDescription) {
|
|
304
304
|
const seq = this.nextSequence(agentId);
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
305
|
+
const legacyChannel = typeof reminder.channel === "string" ? reminder.channel.trim() : "";
|
|
306
|
+
const legacyChatTarget = /^oc_[A-Za-z0-9_-]+$/.test(legacyChannel) ? `chat:${legacyChannel}` : null;
|
|
307
|
+
const deliveryTarget = typeof reminder.deliveryTarget === "string" && reminder.deliveryTarget
|
|
308
|
+
? reminder.deliveryTarget : legacyChatTarget;
|
|
309
|
+
const anchorMessageId = typeof reminder.deliveryAnchor === "string" && /^om_[A-Za-z0-9_-]+$/.test(reminder.deliveryAnchor)
|
|
310
|
+
? reminder.deliveryAnchor
|
|
311
|
+
: typeof reminder.msgRef === "string" && /^om_[A-Za-z0-9_-]+$/.test(reminder.msgRef)
|
|
312
|
+
? reminder.msgRef
|
|
313
|
+
: null;
|
|
314
|
+
const commentAnchorId = deliveryTarget?.startsWith("document-comment:") && typeof reminder.deliveryAnchor === "string"
|
|
315
|
+
&& /^doc_comment_[A-Za-z0-9_-]+$/.test(reminder.deliveryAnchor) ? reminder.deliveryAnchor : null;
|
|
316
|
+
const anchorId = anchorMessageId || commentAnchorId;
|
|
317
|
+
const chatId = deliveryTarget?.match(/^chat:(oc_[A-Za-z0-9_-]+)$/)?.[1] || null;
|
|
308
318
|
const lines = [
|
|
309
319
|
`[定时提醒触发] ${reminder.title}`,
|
|
310
320
|
`提醒ID: #${reminder.reminderId.slice(0, 8)}` + (reminder.repeat && repeatDescription
|
|
311
321
|
? ` 重复: ${repeatDescription}(下次已自动排在 ${reminder.fireAt})`
|
|
312
322
|
: " 类型: 一次性"),
|
|
313
323
|
overdueMs > 120_000 ? `注意: 原定时间已过 ${Math.round(overdueMs / 60_000)} 分钟(Runtime Host 离线期间错过,现补触发)` : null,
|
|
314
|
-
|
|
324
|
+
deliveryTarget ? `原始 deliveryTarget: ${deliveryTarget}`
|
|
325
|
+
: anchorId ? "这是升级前存量 user-facing reminder,优先回复其安全锚点;不得向标题中的任何人或第三方发送消息"
|
|
326
|
+
: "本条为 internal/no-delivery reminder,不得向标题中的任何人或第三方发送消息",
|
|
327
|
+
anchorId ? `锚定消息: ${anchorId}` : reminder.msgRef ? `历史锚点 ${String(reminder.msgRef)} 不是可用的 delivery anchor,不能用于回复` : null,
|
|
315
328
|
anchorMessageId
|
|
316
|
-
?
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
:
|
|
321
|
-
?
|
|
322
|
-
:
|
|
323
|
-
?
|
|
324
|
-
|
|
329
|
+
? [
|
|
330
|
+
`回复原会话: ${this.larkCommand(`im +messages-reply --message-id ${anchorMessageId}${deliveryTarget?.startsWith("thread:") ? " --reply-in-thread" : ""} ...`)}`,
|
|
331
|
+
chatId ? `若 ${anchorMessageId} 无法由当前 Inbox 解析(例如 interaction_* 卡片锚点或已淘汰的历史消息),改用聊天兜底发送: ${this.larkCommand(`im +messages-send --chat-id ${chatId} ...`)};仅使用这个持久化 chat_id,不得猜测收件人` : null,
|
|
332
|
+
].filter((line) => Boolean(line)).join("\n")
|
|
333
|
+
: commentAnchorId
|
|
334
|
+
? `回复原文档评论: ${this.agentCommand(`comment reply --message-id ${commentAnchorId} --text ...`)}`
|
|
335
|
+
: deliveryTarget
|
|
336
|
+
? chatId
|
|
337
|
+
? `发送到原始 target: ${deliveryTarget}: ${this.larkCommand(`im +messages-send --chat-id ${chatId} ...`)}(不得从提醒标题推断收件人)`
|
|
338
|
+
: `发送到原始 target: ${deliveryTarget}(不得从提醒标题推断收件人)`
|
|
339
|
+
: null,
|
|
325
340
|
`这是你之前用 ${this.agentCommand("reminder schedule")} 设置的提醒,请按标题执行相应动作。管理: ${this.agentCommand("reminder list")} / ${this.agentCommand("reminder snooze")} / ${this.agentCommand("reminder cancel")}`,
|
|
326
341
|
].filter((line) => Boolean(line));
|
|
327
342
|
const envelope = {
|
|
328
343
|
kind: "reminder",
|
|
329
|
-
|
|
344
|
+
// The random suffix keeps each firing's occurrence identity unique across
|
|
345
|
+
// Host restarts: the in-memory seq restarts at 1 and would otherwise let a
|
|
346
|
+
// recurring reminder reuse a prior occurrenceId in the delivery audit.
|
|
347
|
+
message_id: `rem_${reminder.reminderId.slice(0, 16)}_${seq}_${this.randomHex(6)}`,
|
|
348
|
+
reminderId: reminder.reminderId,
|
|
330
349
|
seq,
|
|
331
350
|
sender_name: "定时提醒",
|
|
332
351
|
sender_type: "system",
|
|
@@ -336,6 +355,8 @@ export class HostEnvelopeProjector {
|
|
|
336
355
|
timestamp: this.now().toISOString(),
|
|
337
356
|
thread_id: null,
|
|
338
357
|
wake: true,
|
|
358
|
+
deliveryTarget,
|
|
359
|
+
deliveryAnchor: anchorId,
|
|
339
360
|
};
|
|
340
361
|
return { ...envelope, target: RUNTIME_REMINDER_TARGET };
|
|
341
362
|
}
|
|
@@ -93,6 +93,8 @@ class HostInteractionOrchestrator {
|
|
|
93
93
|
const reminderId = `int_${(0, node_crypto_1.createHash)("sha256").update(run.run_id).digest("hex").slice(0, 24)}`;
|
|
94
94
|
const current = this.now();
|
|
95
95
|
const fireAt = effect.args.fire_at ? Date.parse(effect.args.fire_at) : current + Number(effect.args.delay_seconds) * 1_000;
|
|
96
|
+
const deliveryTarget = typeof run.chat_id === "string" && run.chat_id ? `chat:${run.chat_id}` : null;
|
|
97
|
+
const deliveryAnchor = typeof run.message_id === "string" && /^om_[A-Za-z0-9_-]+$/.test(run.message_id) ? run.message_id : null;
|
|
96
98
|
const reminder = ReminderStore.mutate(file, (store) => {
|
|
97
99
|
const existing = store.reminders.find((candidate) => candidate.reminderId === reminderId);
|
|
98
100
|
if (existing)
|
|
@@ -107,6 +109,9 @@ class HostInteractionOrchestrator {
|
|
|
107
109
|
status: "scheduled",
|
|
108
110
|
msgRef: run.message_id,
|
|
109
111
|
msgPermalink: null,
|
|
112
|
+
deliveryTarget,
|
|
113
|
+
deliveryAnchor,
|
|
114
|
+
deliveryMode: "user",
|
|
110
115
|
repeat: null,
|
|
111
116
|
tz: null,
|
|
112
117
|
channel: run.chat_id,
|
|
@@ -114,7 +119,9 @@ class HostInteractionOrchestrator {
|
|
|
114
119
|
version: 1,
|
|
115
120
|
events: [],
|
|
116
121
|
};
|
|
117
|
-
ReminderStore.appendEvent(created, "scheduled", "interaction", run.run_id, created.fireAt, current
|
|
122
|
+
ReminderStore.appendEvent(created, "scheduled", "interaction", run.run_id, created.fireAt, current, {
|
|
123
|
+
deliveryTarget, deliveryAnchor, deliveryMode: "user",
|
|
124
|
+
});
|
|
118
125
|
store.reminders.push(created);
|
|
119
126
|
return created;
|
|
120
127
|
}, Math.max(1, Math.min(350, deadline - this.now() - 100)));
|
|
@@ -57,6 +57,8 @@ export class HostInteractionOrchestrator {
|
|
|
57
57
|
const reminderId = `int_${createHash("sha256").update(run.run_id).digest("hex").slice(0, 24)}`;
|
|
58
58
|
const current = this.now();
|
|
59
59
|
const fireAt = effect.args.fire_at ? Date.parse(effect.args.fire_at) : current + Number(effect.args.delay_seconds) * 1_000;
|
|
60
|
+
const deliveryTarget = typeof run.chat_id === "string" && run.chat_id ? `chat:${run.chat_id}` : null;
|
|
61
|
+
const deliveryAnchor = typeof run.message_id === "string" && /^om_[A-Za-z0-9_-]+$/.test(run.message_id) ? run.message_id : null;
|
|
60
62
|
const reminder = ReminderStore.mutate(file, (store) => {
|
|
61
63
|
const existing = store.reminders.find((candidate) => candidate.reminderId === reminderId);
|
|
62
64
|
if (existing)
|
|
@@ -71,6 +73,9 @@ export class HostInteractionOrchestrator {
|
|
|
71
73
|
status: "scheduled",
|
|
72
74
|
msgRef: run.message_id,
|
|
73
75
|
msgPermalink: null,
|
|
76
|
+
deliveryTarget,
|
|
77
|
+
deliveryAnchor,
|
|
78
|
+
deliveryMode: "user",
|
|
74
79
|
repeat: null,
|
|
75
80
|
tz: null,
|
|
76
81
|
channel: run.chat_id,
|
|
@@ -78,7 +83,9 @@ export class HostInteractionOrchestrator {
|
|
|
78
83
|
version: 1,
|
|
79
84
|
events: [],
|
|
80
85
|
};
|
|
81
|
-
ReminderStore.appendEvent(created, "scheduled", "interaction", run.run_id, created.fireAt, current
|
|
86
|
+
ReminderStore.appendEvent(created, "scheduled", "interaction", run.run_id, created.fireAt, current, {
|
|
87
|
+
deliveryTarget, deliveryAnchor, deliveryMode: "user",
|
|
88
|
+
});
|
|
82
89
|
store.reminders.push(created);
|
|
83
90
|
return created;
|
|
84
91
|
}, Math.max(1, Math.min(350, deadline - this.now() - 100)));
|