This commit is contained in:
Jiang Bohan
2026-02-09 16:25:30 +08:00
5 changed files with 41 additions and 20 deletions

View File

@@ -38,6 +38,7 @@ export default defineConfig({
'@mariozechner/pi-agent-core',
'@mariozechner/pi-ai',
'@mariozechner/pi-coding-agent',
'grammy',
],
},
},

View File

@@ -73,15 +73,25 @@ export class AsyncAgent {
if (result.error) {
console.error(`[AsyncAgent] Agent run error: ${result.error}`);
this.channel.send({ id: uuidv7(), content: `[error] ${result.error}` });
this.agent.emitError(result.error);
// Only emit agent_error for HTTP 401 from the LLM provider so the
// UI shows the "Configure" banner. All other errors (400, tool errors,
// etc.) should flow back to the agent for self-recovery.
if (/\b401\b/.test(result.error)) {
this.agent.emitError(result.error);
}
}
})
.catch((err) => {
const message = err instanceof Error ? err.message : String(err);
console.error(`[AsyncAgent] Agent run exception: ${message}`);
this.channel.send({ id: uuidv7(), content: `[error] ${message}` });
// Also emit through subscriber mechanism so IPC listeners receive the error
this.agent.emitError(message);
// Only emit agent_error for HTTP 401 from the LLM provider so the
// UI shows the "Configure" banner. All other errors (400, tool errors,
// etc.) should flow back to the agent for self-recovery.
const errMsg = err instanceof Error ? err.message : String(err);
if (/\b401\b/.test(errMsg)) {
this.agent.emitError(message);
}
})
.finally(() => {
this.pendingWrites = Math.max(0, this.pendingWrites - 1);

View File

@@ -112,8 +112,15 @@ export class Agent {
this.reasoningMode = options.reasoningMode ?? "stream";
this.output = createAgentOutput({ stdout, stderr: this.stderr, reasoningMode: this.reasoningMode });
// Resolve provider and model from options > env vars > defaults
const defaultProvider = options.provider ?? credentialManager.getLlmProvider() ?? "kimi-coding";
// Load session metadata early so stored provider/model can inform defaults
this.sessionId = options.sessionId ?? uuidv7();
const storedMeta = (() => {
const tempSession = new SessionManager({ sessionId: this.sessionId });
return tempSession.getMeta();
})();
// Resolve provider and model from options > session meta > env vars > defaults
const defaultProvider = options.provider ?? storedMeta?.provider ?? credentialManager.getLlmProvider() ?? "kimi-coding";
if (options.authProfileId) {
const profileProvider = options.authProfileId.includes(":")
? options.authProfileId.split(":")[0]!
@@ -195,16 +202,7 @@ export class Agent {
});
}
this.sessionId = options.sessionId ?? uuidv7();
// 解析 model用于获取 context window
const storedMeta = (() => {
// 临时创建 session 获取 meta避免循环依赖
const tempSession = new SessionManager({ sessionId: this.sessionId });
return tempSession.getMeta();
})();
const effectiveProvider = resolvedModel ? this.resolvedProvider : (options.provider ?? storedMeta?.provider);
const effectiveProvider = this.resolvedProvider;
const effectiveModel = resolvedModel ?? options.model ?? storedMeta?.model;
let model = resolveModel({ ...options, provider: effectiveProvider, model: effectiveModel });
@@ -319,7 +317,7 @@ export class Agent {
}
this.session.saveMeta({
provider: this.agent.state.model?.provider,
provider: this.resolvedProvider,
model: this.agent.state.model?.id,
thinkingLevel: this.agent.state.thinkingLevel,
reasoningMode: this.reasoningMode,
@@ -889,9 +887,9 @@ export class Agent {
// Update internal state
this.resolvedProvider = providerId;
// Update session metadata
// Update session metadata (save original providerId, not alias-resolved)
this.session.saveMeta({
provider: actualProvider,
provider: providerId,
model: model.id,
thinkingLevel: this.agent.state.thinkingLevel,
reasoningMode: this.reasoningMode,

View File

@@ -65,6 +65,18 @@ describe("heartbeat runner", () => {
}
});
it("bypasses empty-heartbeat-file check for cron-triggered wakes", async () => {
const dir = await mkdtemp(path.join(os.tmpdir(), "heartbeat-test-"));
try {
await writeFile(path.join(dir, "heartbeat.md"), "# keep empty\n", "utf-8");
const agent = createStubAgent({ profileDir: dir, replyText: "HEARTBEAT_OK" });
const result = await runHeartbeatOnce({ agent: agent as any, reason: "cron:test-job-id" });
expect(result.status).toBe("ran");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it("runs and returns ran for heartbeat acknowledgements", async () => {
const agent = createStubAgent({ replyText: "HEARTBEAT_OK" });
const result = await runHeartbeatOnce({ agent: agent as any, reason: "manual" });

View File

@@ -156,8 +156,8 @@ export async function runHeartbeatOnce(opts: {
}
try {
const isExecEvent = opts.reason === "exec-event";
if (!isExecEvent && (await isHeartbeatFileEmpty(agent))) {
const isForcedWake = opts.reason === "exec-event" || opts.reason?.startsWith("cron:");
if (!isForcedWake && (await isHeartbeatFileEmpty(agent))) {
emitHeartbeatEvent({
status: "skipped",
reason: "empty-heartbeat-file",