larkin 0.4.2 → 0.4.5
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/README.md +5 -1
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +218 -228
- package/assets/readme/larkin-github-cover.png +0 -0
- package/dist/agent/agent-state-store.cjs +1 -2
- package/dist/agent/agent-state-store.mjs +1 -2
- package/dist/app/agent-config.mjs +73 -6
- package/dist/app/agent-readiness.cjs +22 -2
- package/dist/app/agent-readiness.mjs +20 -2
- package/dist/app/binary-entry.mjs +6 -0
- package/dist/app/cli.mjs +4 -0
- package/dist/app/run.mjs +8 -0
- package/dist/app/runtime-process.mjs +13 -1
- package/dist/dashboard/dashboard-view-model.mjs +12 -3
- package/dist/feishu/host-shell.cjs +34 -10
- package/dist/feishu/host-shell.mjs +35 -11
- package/dist/platform/config.cjs +406 -33
- package/dist/platform/config.mjs +405 -33
- package/dist/platform/process-boundary-trace.cjs +108 -0
- package/dist/platform/process-boundary-trace.mjs +102 -0
- package/dist/runtime/pi-artifact-provenance.cjs +147 -0
- package/dist/runtime/pi-artifact-provenance.mjs +139 -0
- package/dist/runtime/pi-compaction-recovery.cjs +120 -40
- package/dist/runtime/pi-compaction-recovery.mjs +117 -40
- package/dist/runtime/pi-model-catalog.cjs +2 -2
- package/dist/runtime/pi-model-catalog.mjs +2 -2
- package/dist/runtime/pi-profile-migration.cjs +556 -0
- package/dist/runtime/pi-profile-migration.mjs +515 -0
- package/dist/runtime/pi-provider-config.cjs +1 -1
- package/dist/runtime/pi-provider-config.mjs +1 -1
- package/dist/runtime/runtime-adapters.mjs +97 -11
- package/dist/runtime/runtime-host.cjs +162 -39
- package/dist/runtime/runtime-host.mjs +163 -40
- package/dist/runtime/runtime-readiness.cjs +6 -4
- package/dist/runtime/runtime-readiness.mjs +6 -4
- package/package.json +3 -3
|
@@ -11,6 +11,8 @@ import { isChannelReconnecting, projectAgentReadiness } from "./agent-readiness.
|
|
|
11
11
|
import { requestAgentUpsert } from "./local-control.mjs";
|
|
12
12
|
import * as larkinConfig from "../platform/config.mjs";
|
|
13
13
|
import { managedOfficialLarkCli } from "./agent-lark-cli-workspace.mjs";
|
|
14
|
+
import { assertBuiltinPiAgentDirectory, piAgentDirectory } from "../runtime/pi-provider-config.mjs";
|
|
15
|
+
import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
|
|
14
16
|
function safeDocumentCommentCapabilities(configDir, profile) {
|
|
15
17
|
try {
|
|
16
18
|
return {
|
|
@@ -49,15 +51,15 @@ const die = (message) => {
|
|
|
49
51
|
console.error(" 下一步: larkin config --help");
|
|
50
52
|
process.exit(1);
|
|
51
53
|
};
|
|
52
|
-
if (!kind || !["agents", "model", "runtime", "effort", "chats", "config"].includes(kind)) {
|
|
53
|
-
die("用法: larkin agents | larkin config <show|mention|apply> | larkin model [<id>] | larkin runtime [<id>] [--model <id>] | larkin effort [<level>|clear] | larkin chats [free|strict <oc_id>]");
|
|
54
|
+
if (!kind || !["agents", "model", "runtime", "effort", "pi-distribution", "chats", "config"].includes(kind)) {
|
|
55
|
+
die("用法: larkin agents | larkin config <show|mention|apply> | larkin model [<id>] | larkin runtime [<id>] [--model <id>] | larkin pi-distribution [show|builtin|external] | larkin effort [<level>|clear] | larkin chats [free|strict <oc_id>]");
|
|
54
56
|
}
|
|
55
57
|
const allowedValueFlags = {
|
|
56
58
|
agents: new Set(), model: new Set(["--agent"]), runtime: new Set(["--agent", "--model"]),
|
|
57
|
-
effort: new Set(["--agent"]), chats: new Set(["--agent"]), config: new Set(["--agent", "--chat"]),
|
|
59
|
+
effort: new Set(["--agent"]), "pi-distribution": new Set(["--agent", "--snapshot"]), chats: new Set(["--agent"]), config: new Set(["--agent", "--chat"]),
|
|
58
60
|
};
|
|
59
61
|
const allowedBooleanFlags = {
|
|
60
|
-
agents: new Set(["--json"]), model: new Set(), runtime: new Set(), effort: new Set(), chats: new Set(), config: new Set(["--json"]),
|
|
62
|
+
agents: new Set(["--json"]), model: new Set(), runtime: new Set(), effort: new Set(), "pi-distribution": new Set(["--import-external-profile"]), chats: new Set(), config: new Set(["--json"]),
|
|
61
63
|
};
|
|
62
64
|
const parsedValues = new Map();
|
|
63
65
|
const parsedBooleans = new Set();
|
|
@@ -91,6 +93,7 @@ const flagAgent = parsedValues.get("--agent");
|
|
|
91
93
|
const flagModel = parsedValues.get("--model");
|
|
92
94
|
const flagChat = parsedValues.get("--chat");
|
|
93
95
|
const flagJson = parsedBooleans.has("--json");
|
|
96
|
+
const importExternalProfile = parsedBooleans.has("--import-external-profile");
|
|
94
97
|
const value = positionals[0];
|
|
95
98
|
function assertOnlyFlags(valueFlags, booleanFlags = []) {
|
|
96
99
|
const allowedValues = new Set(valueFlags);
|
|
@@ -122,6 +125,11 @@ else if (kind === "effort") {
|
|
|
122
125
|
if (positionals.length > 1)
|
|
123
126
|
die("用法: larkin effort [<level>|clear|default] [--agent <App ID>]");
|
|
124
127
|
}
|
|
128
|
+
else if (kind === "pi-distribution") {
|
|
129
|
+
assertOnlyFlags(["--agent", "--snapshot"], ["--import-external-profile"]);
|
|
130
|
+
if (positionals.length > 1)
|
|
131
|
+
die("用法: larkin pi-distribution [show|builtin|external] [--agent <App ID>] [--snapshot <private-file>]");
|
|
132
|
+
}
|
|
125
133
|
else if (kind === "chats") {
|
|
126
134
|
assertOnlyFlags(["--agent"]);
|
|
127
135
|
if (![0, 2].includes(positionals.length))
|
|
@@ -169,6 +177,21 @@ const loaded = (() => {
|
|
|
169
177
|
const { configDir, file, config } = loaded;
|
|
170
178
|
if (!fs.existsSync(file))
|
|
171
179
|
die("未找到 Larkin 配置,请运行 larkin setup");
|
|
180
|
+
const snapshotFile = parsedValues.get("--snapshot");
|
|
181
|
+
if (kind === "pi-distribution" && value === "rollback") {
|
|
182
|
+
if (importExternalProfile)
|
|
183
|
+
die("--import-external-profile 只允许与 builtin 一起使用");
|
|
184
|
+
if (!snapshotFile || flagAgent || positionals.length !== 1)
|
|
185
|
+
die("用法: larkin pi-distribution rollback --snapshot <private-file>");
|
|
186
|
+
try {
|
|
187
|
+
const result = larkinConfig.rollbackConfig(process.env, snapshotFile);
|
|
188
|
+
say(JSON.stringify({ ok: true, rolledBackAgent: result.agentId, revision: result.revision }));
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
die(`回滚失败:${error instanceof Error ? error.message : String(error)}`);
|
|
192
|
+
}
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
172
195
|
if (kind === "agents") {
|
|
173
196
|
const list = Object.values(config.agents || {});
|
|
174
197
|
const daemon = readProcessState(config.larkinHome).daemon;
|
|
@@ -266,12 +289,19 @@ if (kind === "agents") {
|
|
|
266
289
|
&& !!status?.inboundVerifiedAt
|
|
267
290
|
&& Date.parse(status.inboundVerifiedAt) >= Date.parse(String(daemon.startedAt || 0));
|
|
268
291
|
const commentDiagnostic = projectDocumentCommentDiagnostic(commentCapability, status);
|
|
292
|
+
const projectedReadiness = projectAgentReadiness({ agentId: agent.agentId, daemon, status });
|
|
269
293
|
say(` ${agent.name}${agent.name === config.activeAgent ? " [active]" : ""}`);
|
|
270
294
|
const effectiveModel = status?.session?.runtime === agent.runtime && status.session.model ? status.session.model : agent.model;
|
|
271
295
|
const effectiveEffort = status?.session?.runtime === agent.runtime && status.session.reasoningEffort ? status.session.reasoningEffort : agent.effort;
|
|
272
296
|
say(` runtime=${agent.runtime} model=${effectiveModel}${effectiveModel !== agent.model ? ` stored=${agent.model}` : ""}${effectiveEffort ? ` effort=${effectiveEffort}` : ""}`);
|
|
273
|
-
if (status?.runtimeReadiness)
|
|
274
|
-
|
|
297
|
+
if (status?.runtimeReadiness) {
|
|
298
|
+
const current = projectedReadiness.readiness.runtime_ready;
|
|
299
|
+
const state = status.runtimeReadiness.state === "ready" && !current ? "unavailable" : status.runtimeReadiness.state || "incompatible";
|
|
300
|
+
const staleReason = status.runtimeReadiness.state === "ready" && !current
|
|
301
|
+
? ":ready 证据不属于当前 owned daemon epoch 或当前 session,已拒绝展示为 ready"
|
|
302
|
+
: status.runtimeReadiness.reason ? `:${status.runtimeReadiness.reason}` : "";
|
|
303
|
+
say(` runtime readiness=${state}${staleReason}${status.runtimeReadiness.nextAction ? `;下一步:${status.runtimeReadiness.nextAction}` : ""}`);
|
|
304
|
+
}
|
|
275
305
|
if (status?.runtimeReadiness?.executable)
|
|
276
306
|
say(` runtime executable=${status.runtimeReadiness.executable}${status.runtimeReadiness.version ? ` version=${status.runtimeReadiness.version}` : ""}`);
|
|
277
307
|
say(` bot=${bot ? `${bot.name}(${bot.open_id})` : "(未连接过,无身份缓存)"}`);
|
|
@@ -405,6 +435,43 @@ if (!key) {
|
|
|
405
435
|
}
|
|
406
436
|
const selectedKey = key;
|
|
407
437
|
const agent = config.agents[selectedKey];
|
|
438
|
+
if (kind === "pi-distribution") {
|
|
439
|
+
traceProcessBoundary(process.env, "agent-config:pi-distribution-loaded", { configDir, agentId: selectedKey, targetDir: piAgentDirectory(configDir, selectedKey), requested: value || "show", importExternalProfile });
|
|
440
|
+
const requested = value || "show";
|
|
441
|
+
if (importExternalProfile && requested !== "builtin")
|
|
442
|
+
die("--import-external-profile 只允许与 builtin 一起使用");
|
|
443
|
+
if (requested === "show") {
|
|
444
|
+
say(JSON.stringify({ agentId: selectedKey, runtime: agent.runtime, piDistribution: agent.piDistribution ?? "external" }));
|
|
445
|
+
process.exit(0);
|
|
446
|
+
}
|
|
447
|
+
if (requested !== "builtin" && requested !== "external")
|
|
448
|
+
die("Pi distribution 只允许 show/builtin/external");
|
|
449
|
+
if (agent.runtime !== "pi")
|
|
450
|
+
die(`Agent ${selectedKey} 不是 Pi runtime`);
|
|
451
|
+
if (!snapshotFile)
|
|
452
|
+
die("修改 Pi distribution 必须提供 --snapshot <private-file>");
|
|
453
|
+
if (requested === "builtin" && !importExternalProfile) {
|
|
454
|
+
try {
|
|
455
|
+
assertBuiltinPiAgentDirectory(piAgentDirectory(configDir, selectedKey));
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
die("内置 Pi provider 尚未为该 Agent 配置;请先运行 larkin setup 完成内置 Pi provider 配置,或使用 --import-external-profile");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
const result = larkinConfig.mutateConfig(process.env, { kind: "set-agent-pi-distribution", agentId: selectedKey, distribution: requested }, { kind: "user" }, {
|
|
463
|
+
snapshotFile: snapshotFile, ...(importExternalProfile ? { importExternalProfile: true } : {}),
|
|
464
|
+
});
|
|
465
|
+
traceProcessBoundary(process.env, "agent-config:pi-distribution-persisted", { configDir, agentId: selectedKey, targetDir: piAgentDirectory(configDir, selectedKey), requested, applyState: result.applyState });
|
|
466
|
+
say(JSON.stringify({ ok: true, agentId: selectedKey, piDistribution: requested, revision: result.revision, applyState: result.applyState }));
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
if (importExternalProfile)
|
|
470
|
+
die("Pi external profile import failed; no secret or private path was disclosed");
|
|
471
|
+
die(`Pi distribution 修改失败:${error instanceof Error ? error.message : String(error)}`);
|
|
472
|
+
}
|
|
473
|
+
process.exit(0);
|
|
474
|
+
}
|
|
408
475
|
const catalog = larkinConfig.loadRuntimeModels();
|
|
409
476
|
if ((["model", "effort"].includes(kind || "") && agent.runtime === "codex") || (kind === "runtime" && value === "codex")) {
|
|
410
477
|
try {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isRuntimeReadinessCurrent = isRuntimeReadinessCurrent;
|
|
4
|
+
exports.isCurrentOwnedDaemon = isCurrentOwnedDaemon;
|
|
3
5
|
exports.isChannelReconnecting = isChannelReconnecting;
|
|
4
6
|
exports.projectAgentReadiness = projectAgentReadiness;
|
|
5
7
|
const process_state_js_1 = require("../platform/process-state.cjs");
|
|
@@ -7,6 +9,19 @@ function timestamp(value) {
|
|
|
7
9
|
const parsed = Date.parse(String(value || ""));
|
|
8
10
|
return Number.isFinite(parsed) ? parsed : null;
|
|
9
11
|
}
|
|
12
|
+
function isRuntimeReadinessCurrent(readiness, daemonStartedAt) {
|
|
13
|
+
if (readiness?.state !== "ready")
|
|
14
|
+
return false;
|
|
15
|
+
const observedAt = timestamp(readiness.observedAt);
|
|
16
|
+
const epoch = timestamp(daemonStartedAt);
|
|
17
|
+
return observedAt !== null && epoch !== null && observedAt >= epoch;
|
|
18
|
+
}
|
|
19
|
+
function isCurrentOwnedDaemon(daemon) {
|
|
20
|
+
return daemon?.state === "owned"
|
|
21
|
+
&& Number(daemon.pid) > 0
|
|
22
|
+
&& typeof daemon.processStartToken === "string" && daemon.processStartToken.length > 0
|
|
23
|
+
&& timestamp(daemon.startedAt) !== null;
|
|
24
|
+
}
|
|
10
25
|
function isChannelReconnecting(status) {
|
|
11
26
|
const reconnectingAt = timestamp(status?.reconnectingAt);
|
|
12
27
|
const connectedAt = timestamp(status?.connectedAt);
|
|
@@ -16,9 +31,14 @@ function isChannelReconnecting(status) {
|
|
|
16
31
|
}
|
|
17
32
|
function projectAgentReadiness(input) {
|
|
18
33
|
const status = input.status ?? null;
|
|
19
|
-
const daemonOwned = (0, process_state_js_1.daemonHasAgent)(input.daemon, input.agentId);
|
|
34
|
+
const daemonOwned = isCurrentOwnedDaemon(input.daemon) && (0, process_state_js_1.daemonHasAgent)(input.daemon, input.agentId);
|
|
20
35
|
const daemonStartedAt = timestamp(input.daemon.startedAt);
|
|
21
36
|
const connectedAt = timestamp(status?.connectedAt);
|
|
37
|
+
// A resumed session deliberately retains its original creation time. Its
|
|
38
|
+
// current daemon-epoch proof is the fresh session observation, not that
|
|
39
|
+
// historic creation timestamp.
|
|
40
|
+
const sessionObservedAt = timestamp(status?.session?.lastSeenAt) ?? timestamp(status?.session?.startedAt);
|
|
41
|
+
const sessionCurrent = daemonStartedAt !== null && sessionObservedAt !== null && sessionObservedAt >= daemonStartedAt;
|
|
22
42
|
const channelConnected = daemonOwned
|
|
23
43
|
&& status?.connectedVia === "channel"
|
|
24
44
|
&& daemonStartedAt !== null
|
|
@@ -26,7 +46,7 @@ function projectAgentReadiness(input) {
|
|
|
26
46
|
&& connectedAt >= daemonStartedAt;
|
|
27
47
|
const readiness = {
|
|
28
48
|
daemon_owned: daemonOwned,
|
|
29
|
-
runtime_ready: status?.runtimeReadiness
|
|
49
|
+
runtime_ready: daemonOwned && sessionCurrent && isRuntimeReadinessCurrent(status?.runtimeReadiness, input.daemon.startedAt),
|
|
30
50
|
channel_connected: channelConnected,
|
|
31
51
|
channel_not_reconnecting: !isChannelReconnecting(status),
|
|
32
52
|
};
|
|
@@ -3,6 +3,19 @@ function timestamp(value) {
|
|
|
3
3
|
const parsed = Date.parse(String(value || ""));
|
|
4
4
|
return Number.isFinite(parsed) ? parsed : null;
|
|
5
5
|
}
|
|
6
|
+
export function isRuntimeReadinessCurrent(readiness, daemonStartedAt) {
|
|
7
|
+
if (readiness?.state !== "ready")
|
|
8
|
+
return false;
|
|
9
|
+
const observedAt = timestamp(readiness.observedAt);
|
|
10
|
+
const epoch = timestamp(daemonStartedAt);
|
|
11
|
+
return observedAt !== null && epoch !== null && observedAt >= epoch;
|
|
12
|
+
}
|
|
13
|
+
export function isCurrentOwnedDaemon(daemon) {
|
|
14
|
+
return daemon?.state === "owned"
|
|
15
|
+
&& Number(daemon.pid) > 0
|
|
16
|
+
&& typeof daemon.processStartToken === "string" && daemon.processStartToken.length > 0
|
|
17
|
+
&& timestamp(daemon.startedAt) !== null;
|
|
18
|
+
}
|
|
6
19
|
export function isChannelReconnecting(status) {
|
|
7
20
|
const reconnectingAt = timestamp(status?.reconnectingAt);
|
|
8
21
|
const connectedAt = timestamp(status?.connectedAt);
|
|
@@ -12,9 +25,14 @@ export function isChannelReconnecting(status) {
|
|
|
12
25
|
}
|
|
13
26
|
export function projectAgentReadiness(input) {
|
|
14
27
|
const status = input.status ?? null;
|
|
15
|
-
const daemonOwned = daemonHasAgent(input.daemon, input.agentId);
|
|
28
|
+
const daemonOwned = isCurrentOwnedDaemon(input.daemon) && daemonHasAgent(input.daemon, input.agentId);
|
|
16
29
|
const daemonStartedAt = timestamp(input.daemon.startedAt);
|
|
17
30
|
const connectedAt = timestamp(status?.connectedAt);
|
|
31
|
+
// A resumed session deliberately retains its original creation time. Its
|
|
32
|
+
// current daemon-epoch proof is the fresh session observation, not that
|
|
33
|
+
// historic creation timestamp.
|
|
34
|
+
const sessionObservedAt = timestamp(status?.session?.lastSeenAt) ?? timestamp(status?.session?.startedAt);
|
|
35
|
+
const sessionCurrent = daemonStartedAt !== null && sessionObservedAt !== null && sessionObservedAt >= daemonStartedAt;
|
|
18
36
|
const channelConnected = daemonOwned
|
|
19
37
|
&& status?.connectedVia === "channel"
|
|
20
38
|
&& daemonStartedAt !== null
|
|
@@ -22,7 +40,7 @@ export function projectAgentReadiness(input) {
|
|
|
22
40
|
&& connectedAt >= daemonStartedAt;
|
|
23
41
|
const readiness = {
|
|
24
42
|
daemon_owned: daemonOwned,
|
|
25
|
-
runtime_ready: status?.runtimeReadiness
|
|
43
|
+
runtime_ready: daemonOwned && sessionCurrent && isRuntimeReadinessCurrent(status?.runtimeReadiness, input.daemon.startedAt),
|
|
26
44
|
channel_connected: channelConnected,
|
|
27
45
|
channel_not_reconnecting: !isChannelReconnecting(status),
|
|
28
46
|
};
|
|
@@ -55,6 +55,12 @@ async function dispatchInternal(mode, rest) {
|
|
|
55
55
|
process.title = "pi-rpc";
|
|
56
56
|
process.env.PI_CODING_AGENT = "true";
|
|
57
57
|
process.emitWarning = (() => { });
|
|
58
|
+
// The Pi package's normal Bun CLI registers OAuth loaders before starting.
|
|
59
|
+
// Larkin invokes its RPC entrypoint directly, so do the same here; otherwise
|
|
60
|
+
// an OAuth-backed compact falls back to a dynamic module import unavailable in
|
|
61
|
+
// a compiled standalone binary.
|
|
62
|
+
const { registerBunOAuthFlows } = await import("@earendil-works/pi-ai/bun-oauth");
|
|
63
|
+
registerBunOAuthFlows();
|
|
58
64
|
const { main: piMain } = await import("@earendil-works/pi-coding-agent");
|
|
59
65
|
const { invokeBuiltinPiRpc } = await import("../runtime/pi-inline-extensions.mjs");
|
|
60
66
|
await invokeBuiltinPiRpc(piMain, rest);
|
package/dist/app/cli.mjs
CHANGED
|
@@ -29,6 +29,7 @@ const routes = {
|
|
|
29
29
|
model: ["agent-config", "model"],
|
|
30
30
|
runtime: ["agent-config", "runtime"],
|
|
31
31
|
effort: ["agent-config", "effort"],
|
|
32
|
+
"pi-distribution": ["agent-config", "pi-distribution"],
|
|
32
33
|
chats: ["agent-config", "chats"],
|
|
33
34
|
config: ["agent-config", "config"],
|
|
34
35
|
session: ["session-cli"],
|
|
@@ -58,6 +59,9 @@ List every configured Agent and its current local status. Use --json for daemon/
|
|
|
58
59
|
Show or change an Agent model.`,
|
|
59
60
|
runtime: `Usage: larkin runtime [<runtime>] [--agent <App ID>] [--model <model>]
|
|
60
61
|
Show or change an Agent runtime.`,
|
|
62
|
+
"pi-distribution": `Usage: larkin pi-distribution [show|builtin|external] [--agent <App ID>] [--snapshot <private-file>] [--import-external-profile]
|
|
63
|
+
larkin pi-distribution rollback --snapshot <private-file>
|
|
64
|
+
Show or change one Pi Agent distribution. builtin requires configured provider state, or explicitly imports the external Pi 0.84.2 profile with --import-external-profile; all changes support config-lock CAS rollback.`,
|
|
61
65
|
effort: `Usage: larkin effort [<level>|clear|default] [--agent <App ID>]
|
|
62
66
|
Show or change an Agent reasoning effort; clear/default restores the Runtime default.`,
|
|
63
67
|
chats: `Usage: larkin chats [--agent <App ID>]
|
package/dist/app/run.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { acquireProcessLock, readProcessState, terminateOwnedProcess, waitForPro
|
|
|
11
11
|
import { hydrateRuntimeAgent, syncAgentProfile } 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
|
+
import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
|
|
14
15
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
const die = (message) => { console.error(`✗ ${message}`); process.exit(1); };
|
|
16
17
|
export function selectedAgentIds(argv, available) {
|
|
@@ -63,8 +64,11 @@ export async function stopDashboardWithinBound(child, configDir, signal = "SIGTE
|
|
|
63
64
|
return await waitForChildExit(child, Math.max(250, Math.min(2_000, timeoutMs))) ? "killed" : "unowned";
|
|
64
65
|
}
|
|
65
66
|
export async function main() {
|
|
67
|
+
process.env.LARKIN_DAEMON_EPOCH ||= new Date().toISOString();
|
|
66
68
|
const argv = process.argv.slice(2);
|
|
67
69
|
const { configDir, file: configFile, config } = larkinConfig.loadConfig(process.env);
|
|
70
|
+
for (const agent of Object.values(config.agents))
|
|
71
|
+
traceProcessBoundary(process.env, "supervisor:config-loaded", { configDir, agentId: agent.agentId, targetDir: path.join(configDir, "providers", "pi", agent.agentId) });
|
|
68
72
|
if (!fs.existsSync(configFile))
|
|
69
73
|
die(`没找到配置 ${configFile},先跑 larkin setup`);
|
|
70
74
|
if (!config.serverId || !Object.keys(config.agents).length)
|
|
@@ -193,6 +197,8 @@ export async function main() {
|
|
|
193
197
|
};
|
|
194
198
|
if (argv.includes("--dry-run"))
|
|
195
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) });
|
|
196
202
|
let stopping = false;
|
|
197
203
|
let dashboard = null;
|
|
198
204
|
// Set when the daemon-restart path stops the dashboard on purpose (port
|
|
@@ -218,6 +224,8 @@ export async function main() {
|
|
|
218
224
|
// external process manager (launchd) takes over as the final respawn layer.
|
|
219
225
|
const launchDaemon = () => {
|
|
220
226
|
daemon = spawn(daemonSpec.command, daemonSpec.args, { env: runtimeEnv, stdio: "inherit" });
|
|
227
|
+
for (const agent of agents)
|
|
228
|
+
traceProcessBoundary(runtimeEnv, "supervisor:daemon-spawned", { configDir, agentId: agent.agentId, targetDir: path.join(configDir, "providers", "pi", agent.agentId), childPid: daemon.pid ?? null });
|
|
221
229
|
daemon.once("exit", (code, signal) => {
|
|
222
230
|
if (stopping) {
|
|
223
231
|
resolveDaemonFinal?.({ code, signal });
|
|
@@ -7,6 +7,7 @@ import { createNativeRuntimeAdapter } from "../runtime/runtime-adapters.mjs";
|
|
|
7
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
|
+
import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
|
|
10
11
|
import { createAgentControlServer } from "./local-control.mjs";
|
|
11
12
|
import { hydrateRuntimeAgent, syncAgentProfile } from "./runtime-agent-config.mjs";
|
|
12
13
|
import { loadTelemetryConfig } from "../platform/telemetry-config.mjs";
|
|
@@ -53,6 +54,7 @@ export async function main(env = process.env, overrides = {}) {
|
|
|
53
54
|
channelPackage = await import(pathToFileURL(path.resolve(env.LARKIN_TEST_CHANNEL_MODULE)).href);
|
|
54
55
|
}
|
|
55
56
|
const adapters = new Map();
|
|
57
|
+
env.LARKIN_DAEMON_EPOCH ||= new Date().toISOString();
|
|
56
58
|
const runtimeHost = overrides.runtimeHost ?? createRuntimeHost({
|
|
57
59
|
promptBuilder: new ContextPromptBuilder(),
|
|
58
60
|
stateStoreFor(agentId) {
|
|
@@ -91,6 +93,9 @@ export async function main(env = process.env, overrides = {}) {
|
|
|
91
93
|
throw new Error("LARKIN_HOME/LARKIN_CONFIG_DIR required");
|
|
92
94
|
if (!env.LARKIN_CONTROL_AUTHORIZATION)
|
|
93
95
|
throw new Error("LARKIN_CONTROL_AUTHORIZATION required");
|
|
96
|
+
const loaded = loadConfig(env);
|
|
97
|
+
for (const agent of Object.values(loaded.config.agents))
|
|
98
|
+
traceProcessBoundary(env, "daemon:config-loaded", { configDir: loaded.configDir, agentId: agent.agentId, targetDir: path.join(loaded.configDir, "providers", "pi", agent.agentId) });
|
|
94
99
|
controlServer = createAgentControlServer({
|
|
95
100
|
larkinHome: env.LARKIN_HOME,
|
|
96
101
|
authorityToken: env.LARKIN_CONTROL_AUTHORIZATION,
|
|
@@ -110,7 +115,14 @@ export async function main(env = process.env, overrides = {}) {
|
|
|
110
115
|
},
|
|
111
116
|
});
|
|
112
117
|
await controlServer.start();
|
|
113
|
-
|
|
118
|
+
for (const agent of hostShell.agents)
|
|
119
|
+
traceProcessBoundary(env, "daemon:before-runtime-ready", { configDir: loaded.configDir, agentId: agent.agentId, targetDir: path.join(loaded.configDir, "providers", "pi", agent.agentId) });
|
|
120
|
+
const runtimeStart = hostShell.start().catch((error) => {
|
|
121
|
+
for (const agent of hostShell.agents)
|
|
122
|
+
traceProcessBoundary(env, "daemon:runtime-ready-failure", { configDir: loaded.configDir, agentId: agent.agentId, targetDir: path.join(loaded.configDir, "providers", "pi", agent.agentId), error });
|
|
123
|
+
throw error;
|
|
124
|
+
});
|
|
125
|
+
await markConfigAppliedAfterRuntimeReady(env, hostShell.agents, runtimeStart);
|
|
114
126
|
}
|
|
115
127
|
if (path.resolve(process.argv[1] || "") === path.resolve(fileURLToPath(import.meta.url))) {
|
|
116
128
|
main().catch((error) => { console.error(`✗ ${error instanceof Error ? error.message : String(error)}`); process.exit(1); });
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { StringDecoder } from "node:string_decoder";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { readJson, readProcessState } from "../platform/process-state.mjs";
|
|
10
|
+
import { isRuntimeReadinessCurrent } from "../app/agent-readiness.mjs";
|
|
10
11
|
import { isAllowedDashboardAvatarUrl } from "./dashboard-avatar.mjs";
|
|
11
12
|
import { collectWorkspaceEntry as collectTypedWorkspaceEntry } from "./dashboard-workspace.mjs";
|
|
12
13
|
import { buildFingerprint, packageVersion } from "../platform/build-info.mjs";
|
|
@@ -429,7 +430,7 @@ export function projectStatusTimeline(status) {
|
|
|
429
430
|
].sort((x, y) => new Date(String(y.at)).getTime() - new Date(String(x.at)).getTime()).slice(0, 30);
|
|
430
431
|
return { activityLog, deliverLog, recentErrors, lastActivity, lastDeliver, feed };
|
|
431
432
|
}
|
|
432
|
-
async function collectAgentStatus(a, configDir, piModelResolver) {
|
|
433
|
+
async function collectAgentStatus(a, configDir, daemonStartedAt, piModelResolver) {
|
|
433
434
|
const dir = a.stateDir;
|
|
434
435
|
const botIdentity = readJson(path.join(dir, "bot-identity.json"), null);
|
|
435
436
|
const status = readJson(path.join(dir, "status.json"), {});
|
|
@@ -476,6 +477,14 @@ async function collectAgentStatus(a, configDir, piModelResolver) {
|
|
|
476
477
|
}
|
|
477
478
|
// 本 agent 专属时间线:三路历史合并按时间倒序,不是全局一条汇总——每个 agent 只看自己的故事。
|
|
478
479
|
const { recentErrors, lastActivity, lastDeliver, feed } = projectStatusTimeline(status);
|
|
480
|
+
const statusReadiness = status.runtimeReadiness;
|
|
481
|
+
const sessionStartedAt = finiteTime(status.session?.startedAt);
|
|
482
|
+
const daemonEpoch = finiteTime(daemonStartedAt);
|
|
483
|
+
const sessionCurrent = sessionStartedAt !== null && daemonEpoch !== null && sessionStartedAt >= daemonEpoch;
|
|
484
|
+
const runtimeReadiness = sessionCurrent && isRuntimeReadinessCurrent(statusReadiness, daemonStartedAt)
|
|
485
|
+
|| statusReadiness?.state !== "ready"
|
|
486
|
+
? statusReadiness || null
|
|
487
|
+
: { ...statusReadiness, state: "unavailable", reason: "Runtime readiness evidence is stale for the current daemon epoch." };
|
|
479
488
|
// 提醒:不只给数量,给完整列表——待触发按最近到期优先排前面,已触发/已取消按最近的排在后面。
|
|
480
489
|
const pendingReminders = reminders.filter((r) => r.status === "scheduled" || r.status === "pending")
|
|
481
490
|
.sort((x, y) => new Date(String(x.fireAt)).getTime() - new Date(String(y.fireAt)).getTime());
|
|
@@ -492,7 +501,7 @@ async function collectAgentStatus(a, configDir, piModelResolver) {
|
|
|
492
501
|
runtime: a.runtime,
|
|
493
502
|
model: status.session?.runtime === a.runtime && status.session?.model ? String(status.session.model) : a.model,
|
|
494
503
|
effort: status.session?.runtime === a.runtime && status.session?.reasoningEffort ? String(status.session.reasoningEffort) : a.effort || null,
|
|
495
|
-
runtimeReadiness
|
|
504
|
+
runtimeReadiness,
|
|
496
505
|
credentialReady: fs.existsSync(path.join(configDir, "bots", `${a.feishuProfile}.json`)),
|
|
497
506
|
bot: botIdentity ? {
|
|
498
507
|
name: botIdentity.name,
|
|
@@ -542,7 +551,7 @@ export async function collectStatus(options = {}) {
|
|
|
542
551
|
uptimeSec: daemonStatus?.startedAt ? ageSec(daemonStatus.startedAt) : null,
|
|
543
552
|
agents: Array.isArray(daemonStatus?.agents) ? daemonStatus.agents.filter((id) => typeof id === "string") : [],
|
|
544
553
|
};
|
|
545
|
-
const statuses = (await Promise.all(agents.map((a) => collectAgentStatus(a, configDir, options.piModelResolver)))).map((agent) => {
|
|
554
|
+
const statuses = (await Promise.all(agents.map((a) => collectAgentStatus(a, configDir, daemon.startedAt, options.piModelResolver)))).map((agent) => {
|
|
546
555
|
return { ...agent, ...projectAgentHealth(agent, daemon) };
|
|
547
556
|
});
|
|
548
557
|
return {
|
|
@@ -200,6 +200,11 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
200
200
|
return store;
|
|
201
201
|
};
|
|
202
202
|
const hostState = new host_business_state_js_1.HostStateProjection(stateStore, log);
|
|
203
|
+
const markRuntimeTransition = (agent, reason) => {
|
|
204
|
+
hostState.updateStatus(agent, {
|
|
205
|
+
runtimeReadiness: { runtime: agent.runtime, state: "unavailable", reason, nextAction: "Wait for the current daemon epoch to publish Runtime readiness.", observedAt: new Date().toISOString() },
|
|
206
|
+
});
|
|
207
|
+
};
|
|
203
208
|
const recordInboundDeliveryFailure = (agent, code) => {
|
|
204
209
|
const at = new Date().toISOString();
|
|
205
210
|
const reason = "Inbound Runtime delivery failed non-retryably; durable Inbox/ledger state is retained for recovery.";
|
|
@@ -1231,19 +1236,21 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1231
1236
|
return;
|
|
1232
1237
|
if (message.type === "agent-status") {
|
|
1233
1238
|
log("agent:status", message.agentId, message.status);
|
|
1239
|
+
const observedAt = new Date().toISOString();
|
|
1234
1240
|
if (message.status === "error" || message.status === "inactive")
|
|
1235
1241
|
hostState.updateStatus(agent, {
|
|
1236
|
-
runtimeReadiness: message.readiness?.state && message.readiness.state !== "ready" ? message.readiness : {
|
|
1242
|
+
runtimeReadiness: message.readiness?.state && message.readiness.state !== "ready" ? { ...message.readiness, observedAt } : {
|
|
1237
1243
|
runtime: agent.runtime,
|
|
1238
1244
|
state: message.status === "error" ? "incompatible" : "missing",
|
|
1239
1245
|
reason: message.status === "error" ? message.error || "Runtime entered an error state" : "Runtime is inactive",
|
|
1246
|
+
observedAt,
|
|
1240
1247
|
},
|
|
1241
1248
|
});
|
|
1242
1249
|
else if (message.readiness)
|
|
1243
|
-
hostState.updateStatus(agent, { runtimeReadiness: message.readiness });
|
|
1250
|
+
hostState.updateStatus(agent, { runtimeReadiness: { ...message.readiness, observedAt } });
|
|
1244
1251
|
else if (message.status === "active")
|
|
1245
1252
|
hostState.updateStatus(agent, { runtimeReadiness: {
|
|
1246
|
-
runtime: agent.runtime, state: "ready",
|
|
1253
|
+
runtime: agent.runtime, state: "ready", observedAt,
|
|
1247
1254
|
} });
|
|
1248
1255
|
if (message.status === "active") {
|
|
1249
1256
|
const redeliveryTimer = setTimeout(() => {
|
|
@@ -1375,8 +1382,10 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1375
1382
|
const status = hostState.readStatus(agent);
|
|
1376
1383
|
const turns = status.session && typeof status.session === "object"
|
|
1377
1384
|
? Math.max(0, Number(status.session.turns) || 0) : 0;
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1385
|
+
const sessionStartedAt = status.session && typeof status.session === "object"
|
|
1386
|
+
? Date.parse(String(status.session.startedAt || "")) : Number.NaN;
|
|
1387
|
+
const sessionCurrent = Number.isFinite(sessionStartedAt) && sessionStartedAt >= Date.parse(daemonStartedAt);
|
|
1388
|
+
const runtimeReady = sessionCurrent && (0, agent_readiness_js_1.isRuntimeReadinessCurrent)(status.runtimeReadiness, daemonStartedAt);
|
|
1380
1389
|
const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
|
|
1381
1390
|
return { turns, runtimeReady, ...connectionState(status), pendingCount };
|
|
1382
1391
|
};
|
|
@@ -1385,6 +1394,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1385
1394
|
throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
|
|
1386
1395
|
if (initialProjection.reconnecting)
|
|
1387
1396
|
throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
|
|
1397
|
+
markRuntimeTransition(agent, "Runtime session reset in progress");
|
|
1388
1398
|
let reset;
|
|
1389
1399
|
try {
|
|
1390
1400
|
reset = await runtimeHost.resetSession(agentId);
|
|
@@ -1401,11 +1411,17 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1401
1411
|
delete record.state.sessions[agent.runtime];
|
|
1402
1412
|
record.store.writeJson("agentState", record.state);
|
|
1403
1413
|
}
|
|
1404
|
-
if (!reset.sessionId)
|
|
1414
|
+
if (!reset.sessionId) {
|
|
1415
|
+
const observedAt = new Date().toISOString();
|
|
1405
1416
|
hostState.updateStatus(agent, {
|
|
1406
|
-
session: { runtime: agent.runtime, id: null, launchId: null, startedAt:
|
|
1407
|
-
lastSeenAt: null, lastTurnAt: null, turns: 0 },
|
|
1417
|
+
session: { runtime: agent.runtime, id: null, launchId: null, startedAt: observedAt,
|
|
1418
|
+
lastSeenAt: null, lastTurnAt: null, turns: 0 },
|
|
1419
|
+
runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt },
|
|
1408
1420
|
});
|
|
1421
|
+
}
|
|
1422
|
+
else if (reset.runtimeReady) {
|
|
1423
|
+
hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt: new Date().toISOString() } });
|
|
1424
|
+
}
|
|
1409
1425
|
}
|
|
1410
1426
|
catch (error) {
|
|
1411
1427
|
const projection = readinessProjection();
|
|
@@ -1452,8 +1468,10 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1452
1468
|
const status = hostState.readStatus(agent);
|
|
1453
1469
|
const turns = status.session && typeof status.session === "object"
|
|
1454
1470
|
? Math.max(0, Number(status.session.turns) || 0) : 0;
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1471
|
+
const sessionStartedAt = status.session && typeof status.session === "object"
|
|
1472
|
+
? Date.parse(String(status.session.startedAt || "")) : Number.NaN;
|
|
1473
|
+
const sessionCurrent = Number.isFinite(sessionStartedAt) && sessionStartedAt >= Date.parse(daemonStartedAt);
|
|
1474
|
+
const runtimeReady = sessionCurrent && (0, agent_readiness_js_1.isRuntimeReadinessCurrent)(status.runtimeReadiness, daemonStartedAt);
|
|
1457
1475
|
const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
|
|
1458
1476
|
return { turns, runtimeReady, ...connectionState(status), pendingCount };
|
|
1459
1477
|
};
|
|
@@ -1462,6 +1480,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1462
1480
|
throw Object.assign(new Error(`Agent ${agentId} channel is not connected`), { code: "channel_unavailable", ...initialProjection });
|
|
1463
1481
|
if (initialProjection.reconnecting)
|
|
1464
1482
|
throw Object.assign(new Error(`Agent ${agentId} channel is reconnecting`), { code: "channel_reconnecting", ...initialProjection });
|
|
1483
|
+
markRuntimeTransition(agent, "Runtime session recovery in progress");
|
|
1465
1484
|
let recovery;
|
|
1466
1485
|
try {
|
|
1467
1486
|
recovery = await runtimeHost.recoverSession(agentId, reason);
|
|
@@ -1469,6 +1488,9 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1469
1488
|
catch (error) {
|
|
1470
1489
|
throw Object.assign(error instanceof Error ? error : new Error(String(error)), readinessProjection());
|
|
1471
1490
|
}
|
|
1491
|
+
if (recovery.runtimeReady) {
|
|
1492
|
+
hostState.updateStatus(agent, { runtimeReadiness: { runtime: agent.runtime, state: "ready", observedAt: new Date().toISOString() } });
|
|
1493
|
+
}
|
|
1472
1494
|
const record = agentStates.get(agentId);
|
|
1473
1495
|
try {
|
|
1474
1496
|
if (record) {
|
|
@@ -1506,6 +1528,8 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1506
1528
|
async start() {
|
|
1507
1529
|
try {
|
|
1508
1530
|
node_fs_1.default.mkdirSync(larkinHome, { recursive: true });
|
|
1531
|
+
for (const agent of agents)
|
|
1532
|
+
markRuntimeTransition(agent, "Current daemon epoch starting");
|
|
1509
1533
|
node_fs_1.default.writeFileSync(node_path_1.default.join(larkinHome, "daemon-status.json"), JSON.stringify({
|
|
1510
1534
|
...(0, process_inspect_cjs_1.currentProcessMetadata)((0, internal_command_js_1.processCommandToken)("daemon", "app/runtime-process.mjs")), pid: process.pid, startedAt: daemonStartedAt,
|
|
1511
1535
|
agents: agents.map((agent) => agent.agentId),
|