






















@@ -406,4 +406,269 @@ describe("runtime parity", () => {
406406expect(cell.finalText).toBe("parent scenario final");
407407expect(cell.transcriptBytes).not.toContain("child worker final");
408408});
409+410+it("ignores newer heartbeat-only operational transcripts when selecting the scenario reply", async () => {
411+const tempRoot = await createRuntimeParityGatewayTempRoot([
412+{
413+sessionId: "scenario",
414+updatedAt: 10,
415+transcriptBytes: JSON.stringify({
416+message: {
417+role: "assistant",
418+content: "scenario final",
419+usage: {
420+input: 10,
421+output: 5,
422+totalTokens: 15,
423+},
424+},
425+}),
426+},
427+{
428+sessionId: "heartbeat",
429+updatedAt: 20,
430+transcriptBytes: [
431+JSON.stringify({
432+message: {
433+role: "user",
434+content:
435+"Read HEARTBEAT.md if it exists. If nothing needs attention, reply HEARTBEAT_OK.",
436+},
437+}),
438+JSON.stringify({
439+message: {
440+role: "assistant",
441+content: "HEARTBEAT_OK",
442+usage: {
443+input: 100,
444+output: 50,
445+totalTokens: 150,
446+},
447+},
448+}),
449+].join("\n"),
450+},
451+]);
452+453+const cell = await captureRuntimeParityCell({
454+runtime: "pi",
455+gateway: {
456+ tempRoot,
457+},
458+scenarioResult: {
459+status: "pass",
460+},
461+wallClockMs: 42,
462+});
463+464+expect(cell.finalText).toBe("scenario final");
465+expect(cell.usage.totalTokens).toBe(15);
466+expect(cell.transcriptBytes).not.toContain("HEARTBEAT_OK");
467+});
468+469+it("ignores production heartbeat poll transcripts when selecting the scenario reply", async () => {
470+const tempRoot = await createRuntimeParityGatewayTempRoot([
471+{
472+sessionId: "scenario",
473+updatedAt: 10,
474+transcriptBytes: JSON.stringify({
475+message: {
476+role: "assistant",
477+content: "scenario final",
478+},
479+}),
480+},
481+{
482+sessionId: "heartbeat",
483+updatedAt: 20,
484+transcriptBytes: [
485+JSON.stringify({
486+message: {
487+role: "user",
488+content: "[OpenClaw heartbeat poll]",
489+},
490+}),
491+JSON.stringify({
492+message: {
493+role: "assistant",
494+content: "HEARTBEAT_OK",
495+},
496+}),
497+].join("\n"),
498+},
499+]);
500+501+const cell = await captureRuntimeParityCell({
502+runtime: "pi",
503+gateway: {
504+ tempRoot,
505+},
506+scenarioResult: {
507+status: "pass",
508+},
509+wallClockMs: 42,
510+});
511+512+expect(cell.finalText).toBe("scenario final");
513+expect(cell.transcriptBytes).not.toContain("[OpenClaw heartbeat poll]");
514+});
515+516+it("ignores heartbeat tool-response transcripts when selecting the scenario reply", async () => {
517+const tempRoot = await createRuntimeParityGatewayTempRoot([
518+{
519+sessionId: "scenario",
520+updatedAt: 10,
521+transcriptBytes: JSON.stringify({
522+message: {
523+role: "assistant",
524+content: "scenario final",
525+},
526+}),
527+},
528+{
529+sessionId: "heartbeat-tool",
530+updatedAt: 20,
531+transcriptBytes: [
532+JSON.stringify({
533+message: {
534+role: "user",
535+content: "[OpenClaw heartbeat poll]",
536+},
537+}),
538+JSON.stringify({
539+message: {
540+role: "assistant",
541+content: [
542+{
543+type: "tool_call",
544+id: "call-heartbeat",
545+name: "heartbeat_respond",
546+arguments: {
547+notify: false,
548+outcome: "no_change",
549+summary: "nothing due",
550+},
551+},
552+],
553+},
554+}),
555+JSON.stringify({
556+message: {
557+role: "tool",
558+toolCallId: "call-heartbeat",
559+content: JSON.stringify({ status: "ok" }),
560+},
561+}),
562+].join("\n"),
563+},
564+]);
565+566+const cell = await captureRuntimeParityCell({
567+runtime: "codex",
568+gateway: {
569+ tempRoot,
570+},
571+scenarioResult: {
572+status: "pass",
573+},
574+wallClockMs: 42,
575+});
576+577+expect(cell.finalText).toBe("scenario final");
578+expect(cell.transcriptBytes).not.toContain("heartbeat_respond");
579+});
580+581+it("ignores due-task heartbeats that run ordinary tools before responding", async () => {
582+const tempRoot = await createRuntimeParityGatewayTempRoot([
583+{
584+sessionId: "scenario",
585+updatedAt: 10,
586+transcriptBytes: JSON.stringify({
587+message: {
588+role: "assistant",
589+content: "scenario final",
590+},
591+}),
592+},
593+{
594+sessionId: "heartbeat-tool-check",
595+updatedAt: 20,
596+transcriptBytes: [
597+JSON.stringify({
598+message: {
599+role: "user",
600+content: [
601+{
602+type: "text",
603+text: [
604+"Run the following periodic tasks (only those due based on their intervals):",
605+"",
606+"- status: Check deployment status",
607+"",
608+"After completing all due tasks, use heartbeat_respond to report the outcome.",
609+].join("\n"),
610+},
611+],
612+},
613+}),
614+JSON.stringify({
615+message: {
616+role: "assistant",
617+content: [
618+{
619+type: "toolCall",
620+id: "call-read",
621+name: "read",
622+arguments: { file: "HEARTBEAT.md" },
623+},
624+],
625+},
626+}),
627+JSON.stringify({
628+message: {
629+role: "user",
630+content: [
631+{
632+type: "tool_result",
633+tool_call_id: "call-read",
634+content: "deployment ok",
635+},
636+],
637+},
638+}),
639+JSON.stringify({
640+message: {
641+role: "assistant",
642+content: [
643+{
644+type: "toolCall",
645+id: "call-heartbeat",
646+name: "heartbeat_respond",
647+arguments: {
648+notify: false,
649+outcome: "no_change",
650+summary: "deployment ok",
651+},
652+},
653+],
654+},
655+}),
656+].join("\n"),
657+},
658+]);
659+660+const cell = await captureRuntimeParityCell({
661+runtime: "codex",
662+gateway: {
663+ tempRoot,
664+},
665+scenarioResult: {
666+status: "pass",
667+},
668+wallClockMs: 42,
669+});
670+671+expect(cell.finalText).toBe("scenario final");
672+expect(cell.transcriptBytes).not.toContain("deployment ok");
673+});
409674});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。