@chibitek/chibi 0.1.160 → 0.1.162
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/assets/{abnfDiagram-O67JEVCF-CpuRvzQ3.js → abnfDiagram-O67JEVCF-D0_WX2x_.js} +1 -1
- package/dist/assets/{arc-ko1e0R6h.js → arc-lzBXqTbJ.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-OZ3SsOOa.js → architectureDiagram-NJMV4G6O-KQZbO2M3.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DLRkBkjs.js → blockDiagram-BEXU5L5S-zb3pW64Y.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-e8Q3qMnw.js → c4Diagram-YGBWAQC7-CADaXczd.js} +1 -1
- package/dist/assets/channel-Br57lm8F.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-hCDqaM4Q.js → chunk-2Q5K7J3B-DJP27cEp.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-Dy3Pahu-.js → chunk-3FUC2YCW-Cp9Z7Ybl.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-QMK3iFBF.js → chunk-5VM5RSS4-DjXxom_0.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-6fyp9ERp.js → chunk-GTNCS2PH-DpEJ08zA.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-DWvOhTaO.js → chunk-GWA4HPMP-CQ4Kadr-.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DgohMW2S.js → chunk-JWPE2WC7-CwXmVMdh.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-sRGeD2Y7.js → chunk-NETBCI7D-Vl_kib_R.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CZslD_VF.js → chunk-WEXAMYUT-BhAVbfJe.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-D7ocO3VN.js → chunk-XXDRQBXY-s1a4nKQE.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-T1lLqHHW.js → classDiagram-v2-NBCMYWYE-DeGQSNky.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BdeLiJr3.js → cose-bilkent-JH36ORCC-D5sUHYFG.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BydCA0pv.js → cynefin-OW5HDTMX-DmTvN9T2.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DBxckjwQ.js → cynefinDiagram-VND7K2PF-Cqtt1jCF.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CAsvq6Zk.js → dagre-6A5THRUB-DRZKkSJg.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DTwH3rA_.js → diagram-22UHCM2B-1od4dKOU.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BNYj1bey.js → diagram-3UASUU5V-D3_yCQOJ.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-CihwYFxb.js → diagram-ATOU4E4O-D9UgsVGC.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-nouwk3s2.js → diagram-CDSNMT55-DMxsJ_X5.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BoKewJeA.js → diagram-MLGK6HIB-CK47Pp3D.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CrE1m9DQ.js → diagram-MPIPVDR6-D0axYDk-.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DRj0aU4Y.js → ebnfDiagram-ZINNZB2B-hkbV8j2o.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-Do0JzCG5.js → elk-276RUBZZ-DL9jncwy.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CRsI9pXb.js → erDiagram-OPXOYQCR-etb_6SR6.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-dtD6wm3G.js → flowDiagram-KWPJA3E3-4Dgg4I8f.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-Cn7KyLCo.js → ganttDiagram-FUAMR5RP-DRBB4AaO.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-CGH_VWzq.js → gitGraphDiagram-X574FWY7-CzgKN-tE.js} +1 -1
- package/dist/assets/{index-Bk8iAodB.js → index-BT2giPxq.js} +1 -1
- package/dist/assets/{index-BIcziktH.js → index-fHtcwzVK.js} +179 -179
- package/dist/assets/{infoDiagram-VRGFBTTK-CQKsYTCL.js → infoDiagram-VRGFBTTK-DwpjLCMe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-D5catDe0.js → ishikawaDiagram-OU5B5YK6-H-CWnU1-.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-KjY8mki9.js → journeyDiagram-ZHPQQLJL-CY-oDrpq.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BKCyJtge.js → kanban-definition-PNTS6WVX-DMGIKH3g.js} +1 -1
- package/dist/assets/{linear-B-_obUkq.js → linear-BHhtTLK8.js} +1 -1
- package/dist/assets/{mermaid.core-DlSzuDKt.js → mermaid.core-BWK_Ui3n.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-BFQSaYKh.js → mindmap-definition-NLK3R4M7-Cc17hw2c.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-l51izlev.js → pegDiagram-GJSIUBJH-BqFjUBJK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-6NEXRoZX.js → pieDiagram-5QR66LMP-ikmgTCa3.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BCC9XlOe.js → quadrantDiagram-O4NWA36T-CC3s5Dsc.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-Dd4TmJ86.js → railroadDiagram-XR7U4H2S-Dtwf869F.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-C1E5DaDb.js → requirementDiagram-PLB6GJNP-C5p7AgwQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-Ci6r7abb.js → sankeyDiagram-IPEJSGJF-8NKN770X.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BJ-I6WOh.js → sequenceDiagram-PO4LG4MO-DQucgrWO.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-Cs5QbLOP.js → sizeCapture-INFHLROL-Cxmcj9Nl.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-Bw--rdvq.js → stateDiagram-v2-GCMORJYK-DEdvHXnJ.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-B1O9IEz5.js → swimlanes-2SLR337P-BOGePqj1.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BSe9TZ6t.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-CTZ7DXaw.js → timeline-definition-EJHVYXUP-DfpRxJgE.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DjwgNG0H.js → usecaseDiagram-POWQR4AR-BOmQFDT4.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-DrdJG55h.js → vennDiagram-UO4OBE2U-84pfbvUi.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-HVq776nz.js → wardleyDiagram-VNRHLVJA-Cbxnw8ip.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DPf9VEua.js → xychartDiagram-PMCCYNJV-BLiz8vqY.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/container-mcp.js +7 -6
- package/dist-server/index.js +1176 -927
- package/dist-server/openmausbot.js +31 -9
- package/dist-server/pair-cli.js +31 -9
- package/dist-server/server/checkpoint-recovery.js +18 -0
- package/dist-server/server/config.js +6 -6
- package/dist-server/server/drivers/acp/core.js +17 -17
- package/dist-server/server/engine-failover.js +4 -4
- package/dist-server/server/index.js +9 -8
- package/dist-server/server/routine-mochii-sync.js +252 -0
- package/dist-server/vps-container-mcp.js +7 -6
- package/package.json +1 -1
- package/dist/assets/channel-Lp7erhx-.js +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6wGgYKP.js +0 -8
|
@@ -20622,18 +20622,19 @@ var init_config = __esm({
|
|
|
20622
20622
|
* marketplace checks) and personal skills (~/.agents/skills,
|
|
20623
20623
|
* $CODEX_HOME/skills). */
|
|
20624
20624
|
codexUserMcp: external_exports.boolean().optional(),
|
|
20625
|
-
/** LLM-generated titles for new bot threads. Off until explicitly
|
|
20626
|
-
*
|
|
20627
|
-
* snippet in place — see llmThreadTitlesEnabled. */
|
|
20625
|
+
/** LLM-generated titles for new bot threads. Off until explicitly enabled; a one-shot
|
|
20626
|
+
* that fails or answers junk leaves the first-message snippet in place — see llmThreadTitlesEnabled. */
|
|
20628
20627
|
llmThreadTitles: external_exports.boolean().optional(),
|
|
20629
|
-
/** Small internal jobs (thread titles) run on Apple Intelligence on this
|
|
20630
|
-
|
|
20631
|
-
|
|
20628
|
+
/** Small internal jobs (thread titles) run on Apple Intelligence on this Mac when available. On unless false. */
|
|
20629
|
+
onDeviceHelpers: external_exports.boolean().optional(),
|
|
20630
|
+
/** Mirror routines into private Mochii tasks (server/routine-mochii-sync.ts). Off unless true; no Settings toggle. */
|
|
20631
|
+
routineMochiiSync: external_exports.boolean().optional()
|
|
20632
20632
|
});
|
|
20633
20633
|
addonsConfigSchema = external_exports.object(
|
|
20634
20634
|
Object.fromEntries(ADDONS.map((addon) => [addon.id, external_exports.boolean().optional()]))
|
|
20635
20635
|
).strict();
|
|
20636
20636
|
setupProgressSchema = external_exports.object({
|
|
20637
|
+
foundationRoles: external_exports.array(external_exports.enum(["assistant", "inbox", "research", "coder", "community", "ops"])).max(3).optional(),
|
|
20637
20638
|
picks: external_exports.array(external_exports.string().trim().min(1).max(40)).max(20).optional(),
|
|
20638
20639
|
chibitek: external_exports.boolean().optional(),
|
|
20639
20640
|
steps: external_exports.record(external_exports.string().trim().min(1).max(60), external_exports.enum(["done", "skipped"])).refine((steps) => Object.keys(steps).length <= 60, "too many setup steps").optional(),
|
|
@@ -25444,6 +25445,19 @@ var init_mcp_tool_ref = __esm({
|
|
|
25444
25445
|
}
|
|
25445
25446
|
});
|
|
25446
25447
|
|
|
25448
|
+
// server/checkpoint-recovery.ts
|
|
25449
|
+
function isBrokenAntigravityCheckpoint(driverKind, message2) {
|
|
25450
|
+
return driverKind === "antigravityAgent" && message2.includes("could not find doneCh for checkpoint");
|
|
25451
|
+
}
|
|
25452
|
+
function isAntigravityCheckpointReply(driverKind, text4) {
|
|
25453
|
+
return driverKind === "antigravityAgent" && /^Agent execution error: could not find doneCh for checkpoint\s*$/.test(text4.trim());
|
|
25454
|
+
}
|
|
25455
|
+
var init_checkpoint_recovery = __esm({
|
|
25456
|
+
"server/checkpoint-recovery.ts"() {
|
|
25457
|
+
"use strict";
|
|
25458
|
+
}
|
|
25459
|
+
});
|
|
25460
|
+
|
|
25447
25461
|
// server/resume-recovery.ts
|
|
25448
25462
|
function classifyResumeFailure(state2) {
|
|
25449
25463
|
if (state2.producedOutput || state2.promptSubmitted) return "after-accept";
|
|
@@ -26428,6 +26442,12 @@ function createAcpDriver(support12) {
|
|
|
26428
26442
|
session.acp.failAll(new Error("turn settled"));
|
|
26429
26443
|
session.current = null;
|
|
26430
26444
|
active.delete(threadId);
|
|
26445
|
+
if (isAntigravityCheckpointReply(DRIVER_KIND8, current.state.text)) {
|
|
26446
|
+
ok = false;
|
|
26447
|
+
stopReason = "invalid_checkpoint";
|
|
26448
|
+
emit({ ...base3(threadId, current.turnId), type: "runtime.error", message: current.state.text.trim() });
|
|
26449
|
+
closeSession(threadId, "invalid-checkpoint");
|
|
26450
|
+
}
|
|
26431
26451
|
current.flushAssistantText();
|
|
26432
26452
|
let finalOk = ok;
|
|
26433
26453
|
let finalStopReason = stopReason;
|
|
@@ -27234,7 +27254,8 @@ ${instructed.text}` : instructed.text;
|
|
|
27234
27254
|
type: "runtime.error",
|
|
27235
27255
|
message: errorMessage
|
|
27236
27256
|
});
|
|
27237
|
-
|
|
27257
|
+
if (isBrokenAntigravityCheckpoint(DRIVER_KIND8, errorMessage)) closeSession(threadId, "invalid-checkpoint");
|
|
27258
|
+
settle(threadId, session, false, isBrokenAntigravityCheckpoint(DRIVER_KIND8, errorMessage) ? "invalid_checkpoint" : reason ?? "failed");
|
|
27238
27259
|
}
|
|
27239
27260
|
} catch (e) {
|
|
27240
27261
|
if (!state2.settled) {
|
|
@@ -27251,10 +27272,10 @@ ${instructed.text}` : instructed.text;
|
|
|
27251
27272
|
...needsAuth ? { setup: true } : {},
|
|
27252
27273
|
...failure ? { failure } : {}
|
|
27253
27274
|
});
|
|
27254
|
-
if ((logAuth || !needsAuth && (stalled || e?.acpSessionFailure === true || code === "upstream_outage")) && session.child.exitCode === null && !session.closing) {
|
|
27275
|
+
if ((logAuth || !needsAuth && (stalled || isBrokenAntigravityCheckpoint(DRIVER_KIND8, message2) || e?.acpSessionFailure === true || code === "upstream_outage")) && session.child.exitCode === null && !session.closing) {
|
|
27255
27276
|
closeSession(threadId, logAuth ? "provider-auth" : stalled ? "prompt-stall" : "rpc-failure");
|
|
27256
27277
|
}
|
|
27257
|
-
settle(threadId, session, false, needsAuth ? "auth_required" : "rpc_error");
|
|
27278
|
+
settle(threadId, session, false, needsAuth ? "auth_required" : isBrokenAntigravityCheckpoint(DRIVER_KIND8, message2) ? "invalid_checkpoint" : "rpc_error");
|
|
27258
27279
|
}
|
|
27259
27280
|
}
|
|
27260
27281
|
})();
|
|
@@ -27343,6 +27364,7 @@ var init_core3 = __esm({
|
|
|
27343
27364
|
init_mcp_tool_images();
|
|
27344
27365
|
init_mcp_tool_ref();
|
|
27345
27366
|
init_redact2();
|
|
27367
|
+
init_checkpoint_recovery();
|
|
27346
27368
|
init_resume_recovery();
|
|
27347
27369
|
init_retry();
|
|
27348
27370
|
init_engine_failure();
|
package/dist-server/pair-cli.js
CHANGED
|
@@ -20622,18 +20622,19 @@ var init_config = __esm({
|
|
|
20622
20622
|
* marketplace checks) and personal skills (~/.agents/skills,
|
|
20623
20623
|
* $CODEX_HOME/skills). */
|
|
20624
20624
|
codexUserMcp: external_exports.boolean().optional(),
|
|
20625
|
-
/** LLM-generated titles for new bot threads. Off until explicitly
|
|
20626
|
-
*
|
|
20627
|
-
* snippet in place — see llmThreadTitlesEnabled. */
|
|
20625
|
+
/** LLM-generated titles for new bot threads. Off until explicitly enabled; a one-shot
|
|
20626
|
+
* that fails or answers junk leaves the first-message snippet in place — see llmThreadTitlesEnabled. */
|
|
20628
20627
|
llmThreadTitles: external_exports.boolean().optional(),
|
|
20629
|
-
/** Small internal jobs (thread titles) run on Apple Intelligence on this
|
|
20630
|
-
|
|
20631
|
-
|
|
20628
|
+
/** Small internal jobs (thread titles) run on Apple Intelligence on this Mac when available. On unless false. */
|
|
20629
|
+
onDeviceHelpers: external_exports.boolean().optional(),
|
|
20630
|
+
/** Mirror routines into private Mochii tasks (server/routine-mochii-sync.ts). Off unless true; no Settings toggle. */
|
|
20631
|
+
routineMochiiSync: external_exports.boolean().optional()
|
|
20632
20632
|
});
|
|
20633
20633
|
addonsConfigSchema = external_exports.object(
|
|
20634
20634
|
Object.fromEntries(ADDONS.map((addon) => [addon.id, external_exports.boolean().optional()]))
|
|
20635
20635
|
).strict();
|
|
20636
20636
|
setupProgressSchema = external_exports.object({
|
|
20637
|
+
foundationRoles: external_exports.array(external_exports.enum(["assistant", "inbox", "research", "coder", "community", "ops"])).max(3).optional(),
|
|
20637
20638
|
picks: external_exports.array(external_exports.string().trim().min(1).max(40)).max(20).optional(),
|
|
20638
20639
|
chibitek: external_exports.boolean().optional(),
|
|
20639
20640
|
steps: external_exports.record(external_exports.string().trim().min(1).max(60), external_exports.enum(["done", "skipped"])).refine((steps) => Object.keys(steps).length <= 60, "too many setup steps").optional(),
|
|
@@ -25444,6 +25445,19 @@ var init_mcp_tool_ref = __esm({
|
|
|
25444
25445
|
}
|
|
25445
25446
|
});
|
|
25446
25447
|
|
|
25448
|
+
// server/checkpoint-recovery.ts
|
|
25449
|
+
function isBrokenAntigravityCheckpoint(driverKind, message2) {
|
|
25450
|
+
return driverKind === "antigravityAgent" && message2.includes("could not find doneCh for checkpoint");
|
|
25451
|
+
}
|
|
25452
|
+
function isAntigravityCheckpointReply(driverKind, text4) {
|
|
25453
|
+
return driverKind === "antigravityAgent" && /^Agent execution error: could not find doneCh for checkpoint\s*$/.test(text4.trim());
|
|
25454
|
+
}
|
|
25455
|
+
var init_checkpoint_recovery = __esm({
|
|
25456
|
+
"server/checkpoint-recovery.ts"() {
|
|
25457
|
+
"use strict";
|
|
25458
|
+
}
|
|
25459
|
+
});
|
|
25460
|
+
|
|
25447
25461
|
// server/resume-recovery.ts
|
|
25448
25462
|
function classifyResumeFailure(state2) {
|
|
25449
25463
|
if (state2.producedOutput || state2.promptSubmitted) return "after-accept";
|
|
@@ -26428,6 +26442,12 @@ function createAcpDriver(support12) {
|
|
|
26428
26442
|
session.acp.failAll(new Error("turn settled"));
|
|
26429
26443
|
session.current = null;
|
|
26430
26444
|
active.delete(threadId);
|
|
26445
|
+
if (isAntigravityCheckpointReply(DRIVER_KIND8, current.state.text)) {
|
|
26446
|
+
ok = false;
|
|
26447
|
+
stopReason = "invalid_checkpoint";
|
|
26448
|
+
emit({ ...base3(threadId, current.turnId), type: "runtime.error", message: current.state.text.trim() });
|
|
26449
|
+
closeSession(threadId, "invalid-checkpoint");
|
|
26450
|
+
}
|
|
26431
26451
|
current.flushAssistantText();
|
|
26432
26452
|
let finalOk = ok;
|
|
26433
26453
|
let finalStopReason = stopReason;
|
|
@@ -27234,7 +27254,8 @@ ${instructed.text}` : instructed.text;
|
|
|
27234
27254
|
type: "runtime.error",
|
|
27235
27255
|
message: errorMessage
|
|
27236
27256
|
});
|
|
27237
|
-
|
|
27257
|
+
if (isBrokenAntigravityCheckpoint(DRIVER_KIND8, errorMessage)) closeSession(threadId, "invalid-checkpoint");
|
|
27258
|
+
settle(threadId, session, false, isBrokenAntigravityCheckpoint(DRIVER_KIND8, errorMessage) ? "invalid_checkpoint" : reason ?? "failed");
|
|
27238
27259
|
}
|
|
27239
27260
|
} catch (e) {
|
|
27240
27261
|
if (!state2.settled) {
|
|
@@ -27251,10 +27272,10 @@ ${instructed.text}` : instructed.text;
|
|
|
27251
27272
|
...needsAuth ? { setup: true } : {},
|
|
27252
27273
|
...failure ? { failure } : {}
|
|
27253
27274
|
});
|
|
27254
|
-
if ((logAuth || !needsAuth && (stalled || e?.acpSessionFailure === true || code === "upstream_outage")) && session.child.exitCode === null && !session.closing) {
|
|
27275
|
+
if ((logAuth || !needsAuth && (stalled || isBrokenAntigravityCheckpoint(DRIVER_KIND8, message2) || e?.acpSessionFailure === true || code === "upstream_outage")) && session.child.exitCode === null && !session.closing) {
|
|
27255
27276
|
closeSession(threadId, logAuth ? "provider-auth" : stalled ? "prompt-stall" : "rpc-failure");
|
|
27256
27277
|
}
|
|
27257
|
-
settle(threadId, session, false, needsAuth ? "auth_required" : "rpc_error");
|
|
27278
|
+
settle(threadId, session, false, needsAuth ? "auth_required" : isBrokenAntigravityCheckpoint(DRIVER_KIND8, message2) ? "invalid_checkpoint" : "rpc_error");
|
|
27258
27279
|
}
|
|
27259
27280
|
}
|
|
27260
27281
|
})();
|
|
@@ -27343,6 +27364,7 @@ var init_core3 = __esm({
|
|
|
27343
27364
|
init_mcp_tool_images();
|
|
27344
27365
|
init_mcp_tool_ref();
|
|
27345
27366
|
init_redact2();
|
|
27367
|
+
init_checkpoint_recovery();
|
|
27346
27368
|
init_resume_recovery();
|
|
27347
27369
|
init_retry();
|
|
27348
27370
|
init_engine_failure();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** This identifies an unusable saved session, never permission to replay a
|
|
2
|
+
* submitted prompt. Only a subsequent explicit turn may rebuild the context. */
|
|
3
|
+
export function isBrokenAntigravityCheckpoint(driverKind, message) {
|
|
4
|
+
return driverKind === "antigravityAgent" && message.includes("could not find doneCh for checkpoint");
|
|
5
|
+
}
|
|
6
|
+
export function invalidateBrokenCheckpoint(store, botId, driverKind, event) {
|
|
7
|
+
if (event.providerInstanceId && isBrokenAntigravityCheckpoint(driverKind, event.message)) {
|
|
8
|
+
// Undefined is absent on disk and engineIsFresh rebuilds from Atomic's
|
|
9
|
+
// transcript on the next send, including after an application restart.
|
|
10
|
+
store.setResumeCursor(botId, event.providerInstanceId, undefined, event.threadId);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Antigravity also sends its execution failure as a chunked assistant reply
|
|
14
|
+
* followed by end_turn. A quoted error inside a normal answer is not failure. */
|
|
15
|
+
export function isAntigravityCheckpointReply(driverKind, text) {
|
|
16
|
+
return driverKind === "antigravityAgent"
|
|
17
|
+
&& /^Agent execution error: could not find doneCh for checkpoint\s*$/.test(text.trim());
|
|
18
|
+
}
|
|
@@ -277,13 +277,13 @@ const featureConfigSchema = z.object({
|
|
|
277
277
|
* marketplace checks) and personal skills (~/.agents/skills,
|
|
278
278
|
* $CODEX_HOME/skills). */
|
|
279
279
|
codexUserMcp: z.boolean().optional(),
|
|
280
|
-
/** LLM-generated titles for new bot threads. Off until explicitly
|
|
281
|
-
*
|
|
282
|
-
* snippet in place — see llmThreadTitlesEnabled. */
|
|
280
|
+
/** LLM-generated titles for new bot threads. Off until explicitly enabled; a one-shot
|
|
281
|
+
* that fails or answers junk leaves the first-message snippet in place — see llmThreadTitlesEnabled. */
|
|
283
282
|
llmThreadTitles: z.boolean().optional(),
|
|
284
|
-
/** Small internal jobs (thread titles) run on Apple Intelligence on this
|
|
285
|
-
* Mac when it is available. On unless set to false. */
|
|
283
|
+
/** Small internal jobs (thread titles) run on Apple Intelligence on this Mac when available. On unless false. */
|
|
286
284
|
onDeviceHelpers: z.boolean().optional(),
|
|
285
|
+
/** Mirror routines into private Mochii tasks (server/routine-mochii-sync.ts). Off unless true; no Settings toggle. */
|
|
286
|
+
routineMochiiSync: z.boolean().optional(),
|
|
287
287
|
});
|
|
288
288
|
/** Settings → Add-ons: one workspace switch per bundled add-on
|
|
289
289
|
* (shared/addons.ts). Absent means the add-on's own default. Strict, so a
|
|
@@ -293,8 +293,8 @@ const addonsConfigSchema = z.object(Object.fromEntries(ADDONS.map((addon) => [ad
|
|
|
293
293
|
* it survives cleared site data and is shared by every paired client. Hint
|
|
294
294
|
* ids are short renderer-chosen slugs; the list is capped so a buggy client
|
|
295
295
|
* cannot grow the file without bound. */
|
|
296
|
-
/** The setup wizard's progress (src/lib/setup-wizard.ts SetupProgress). */
|
|
297
296
|
const setupProgressSchema = z.object({
|
|
297
|
+
foundationRoles: z.array(z.enum(["assistant", "inbox", "research", "coder", "community", "ops"])).max(3).optional(),
|
|
298
298
|
picks: z.array(z.string().trim().min(1).max(40)).max(20).optional(),
|
|
299
299
|
chibitek: z.boolean().optional(),
|
|
300
300
|
steps: z.record(z.string().trim().min(1).max(60), z.enum(["done", "skipped"]))
|
|
@@ -48,6 +48,7 @@ import { commandSummary, toolDetailPreview } from "../../tool-summary.js";
|
|
|
48
48
|
import { extractMcpImages } from "../../mcp-tool-images.js";
|
|
49
49
|
import { mcpToolRefFromLabel } from "../../../shared/mcp-tool-ref.js";
|
|
50
50
|
import { redactSecretsInText } from "../../redact.js";
|
|
51
|
+
import { isBrokenAntigravityCheckpoint, isAntigravityCheckpointReply } from "../../checkpoint-recovery.js";
|
|
51
52
|
import { recoveryPromptFor } from "../../resume-recovery.js";
|
|
52
53
|
import { isModelRejection } from "../retry.js";
|
|
53
54
|
import { engineFailureFromCode, isCredentialDead, isOpaqueRequestRejection, logAuthFailure, parseResetAt, resetHintFromData } from "../../engine-failure.js";
|
|
@@ -464,14 +465,15 @@ export function createAcpDriver(support) {
|
|
|
464
465
|
// turn synchronously must find this session free
|
|
465
466
|
session.current = null;
|
|
466
467
|
active.delete(threadId);
|
|
468
|
+
if (isAntigravityCheckpointReply(DRIVER_KIND, current.state.text)) {
|
|
469
|
+
ok = false;
|
|
470
|
+
stopReason = "invalid_checkpoint";
|
|
471
|
+
emit({ ...base(threadId, current.turnId), type: "runtime.error", message: current.state.text.trim() });
|
|
472
|
+
closeSession(threadId, "invalid-checkpoint");
|
|
473
|
+
}
|
|
467
474
|
current.flushAssistantText();
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
// exactly that (a provider may cut a reasoning-only stream and
|
|
471
|
-
// still answer end_turn), and ok:true would end the thread quietly
|
|
472
|
-
// while the person's message went unanswered. Keep the completion,
|
|
473
|
-
// but report it as a failure so terminal chips, incidents and
|
|
474
|
-
// follow-ups see what happened.
|
|
475
|
+
// A successful end_turn without a reply or tool result is a lost
|
|
476
|
+
// turn. Report failure so terminal chips and follow-ups reflect it.
|
|
475
477
|
let finalOk = ok;
|
|
476
478
|
let finalStopReason = stopReason;
|
|
477
479
|
if (finalOk && finalStopReason === null && !current.state.producedItem) {
|
|
@@ -1467,7 +1469,9 @@ export function createAcpDriver(support) {
|
|
|
1467
1469
|
type: "runtime.error",
|
|
1468
1470
|
message: errorMessage,
|
|
1469
1471
|
});
|
|
1470
|
-
|
|
1472
|
+
if (isBrokenAntigravityCheckpoint(DRIVER_KIND, errorMessage))
|
|
1473
|
+
closeSession(threadId, "invalid-checkpoint");
|
|
1474
|
+
settle(threadId, session, false, isBrokenAntigravityCheckpoint(DRIVER_KIND, errorMessage) ? "invalid_checkpoint" : reason ?? "failed");
|
|
1471
1475
|
}
|
|
1472
1476
|
}
|
|
1473
1477
|
catch (e) {
|
|
@@ -1492,18 +1496,14 @@ export function createAcpDriver(support) {
|
|
|
1492
1496
|
...(needsAuth ? { setup: true } : {}),
|
|
1493
1497
|
...(failure ? { failure } : {}),
|
|
1494
1498
|
});
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
//
|
|
1498
|
-
|
|
1499
|
-
// resume the recorded session on a fresh process.
|
|
1500
|
-
// An agent refused by its provider mid-prompt is still running
|
|
1501
|
-
// (retrying on its own): it goes too.
|
|
1502
|
-
if ((logAuth || (!needsAuth && (stalled || e?.acpSessionFailure === true
|
|
1499
|
+
// Evict unusable sessions without replaying accepted prompts.
|
|
1500
|
+
// The harness invalidates broken checkpoints durably so the
|
|
1501
|
+
// next explicit turn rebuilds from the retained transcript.
|
|
1502
|
+
if ((logAuth || (!needsAuth && (stalled || isBrokenAntigravityCheckpoint(DRIVER_KIND, message) || e?.acpSessionFailure === true
|
|
1503
1503
|
|| code === "upstream_outage"))) && session.child.exitCode === null && !session.closing) {
|
|
1504
1504
|
closeSession(threadId, logAuth ? "provider-auth" : stalled ? "prompt-stall" : "rpc-failure");
|
|
1505
1505
|
}
|
|
1506
|
-
settle(threadId, session, false, needsAuth ? "auth_required" : "rpc_error");
|
|
1506
|
+
settle(threadId, session, false, needsAuth ? "auth_required" : isBrokenAntigravityCheckpoint(DRIVER_KIND, message) ? "invalid_checkpoint" : "rpc_error");
|
|
1507
1507
|
}
|
|
1508
1508
|
}
|
|
1509
1509
|
})();
|
|
@@ -49,10 +49,10 @@ export const NO_ANSWER_FAILURE = "The engine ended the turn without an answer.";
|
|
|
49
49
|
function unrecognisedFailure(turn) {
|
|
50
50
|
return { kind: "provider_error", message: turn.lastError ?? NO_ANSWER_FAILURE };
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
* turn
|
|
55
|
-
const NOT_A_PROVIDER_FAILURE = new Set(["interrupted", "cancelled"]);
|
|
52
|
+
/** Stops that must not dispatch the request again: cancellation or a broken
|
|
53
|
+
* checkpoint whose accepted prompt may already have executed actions.
|
|
54
|
+
* The next explicit turn rebuilds that checkpoint from retained history. */
|
|
55
|
+
const NOT_A_PROVIDER_FAILURE = new Set(["interrupted", "cancelled", "invalid_checkpoint"]);
|
|
56
56
|
export class FailoverGate {
|
|
57
57
|
turns = new Map();
|
|
58
58
|
onFailure;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { invalidateBrokenCheckpoint } from "./checkpoint-recovery.js";
|
|
1
2
|
import { createModelExpectation, ModelEvidenceLedger } from "./model-evidence-context.js";
|
|
2
3
|
import { reactToMessage } from "./message-reactions.js";
|
|
3
4
|
import { hasUnboundGoalGuard, goalGuardForEvent, addGoalGuard, removeGoalGuard, runPersonalGoalTurn, updatePersonalGoalReceipt } from "./personal-goal-turn.js";
|
|
@@ -175,6 +176,7 @@ import { agentBrowserFrame, agentBrowserIntegration, browserEngineEncryptionKey,
|
|
|
175
176
|
import { withZipListings } from "./zip-listing.js";
|
|
176
177
|
import { createScreenFrameSource, startPeriodicCapture } from "./screen-frame-source.js";
|
|
177
178
|
import { screenFrameHash, screenSurfaceForTool, screenTouchingTool, settledFrameIsNews } from "./screen-frame-gate.js";
|
|
179
|
+
import { RoutineMochiiSync } from "./routine-mochii-sync.js";
|
|
178
180
|
import { RoutineRequestService } from "./routine-requests.js";
|
|
179
181
|
import { buildBotOverview } from "./bot-overview.js";
|
|
180
182
|
import { ProfileRequestService } from "./profile-requests.js";
|
|
@@ -2430,6 +2432,7 @@ function noteNexusLocalMemory(botId) {
|
|
|
2430
2432
|
}
|
|
2431
2433
|
}
|
|
2432
2434
|
nexusMemory.start();
|
|
2435
|
+
const routineMochiiSync = RoutineMochiiSync.start({ dataDir: DATA_DIR, config: () => cfg, listRoutines: () => routines?.listRoutines() ?? [], listRuns: () => routines?.listRuns() ?? [], botName: (id) => store.bot(id)?.name, timeZone: () => routineTimeZone() });
|
|
2433
2436
|
// Register the Private Bot's Nexus key as soon as there is a Private Bot, so
|
|
2434
2437
|
// nothing else can register first (Nexus trusts the first key).
|
|
2435
2438
|
// (Deferred: the store is created further down.)
|
|
@@ -8170,22 +8173,20 @@ bus.subscribe((event) => {
|
|
|
8170
8173
|
});
|
|
8171
8174
|
break;
|
|
8172
8175
|
case "runtime.error":
|
|
8176
|
+
if (bot)
|
|
8177
|
+
invalidateBrokenCheckpoint(store, bot.id, event.providerInstanceId ? registry.get(event.providerInstanceId)?.driverKind : undefined, event);
|
|
8173
8178
|
if (event.message?.trim())
|
|
8174
8179
|
lastTurnError.set(event.threadId, event.message.trim().slice(0, 300));
|
|
8175
8180
|
pushMessage({
|
|
8176
8181
|
role: "bot",
|
|
8177
8182
|
kind: "activity",
|
|
8178
|
-
//
|
|
8179
|
-
// same way; an error line is never a lost connection either way.
|
|
8183
|
+
// Keep the category consistent across surfaces.
|
|
8180
8184
|
// The harness's own stop line is kept whole: it ends with where to fix
|
|
8181
8185
|
// the account, which a 160-character cut would drop.
|
|
8182
8186
|
tool: { name: `error: ${event.message.slice(0, event.category === "account" || event.category === "failover_exhausted" ? 600 : 160)}`, ok: false, setup: event.setup, ...(event.terminal ? { terminal: true } : {}),
|
|
8183
8187
|
...(event.category ? { errorCategory: event.category } : {}) },
|
|
8184
8188
|
});
|
|
8185
|
-
//
|
|
8186
|
-
// dead until something changes, not merely idle. The next successful
|
|
8187
|
-
// dispatch moves it to working; turn.completed (which follows a setup
|
|
8188
|
-
// failure) is told to leave "dead" alone.
|
|
8189
|
+
// Setup failures remain dead until a later successful dispatch.
|
|
8189
8190
|
if (event.setup && bot)
|
|
8190
8191
|
store.setTaskActivity(bot.id, event.threadId, "dead");
|
|
8191
8192
|
break;
|
|
@@ -11274,7 +11275,7 @@ routines = new RoutineManager({
|
|
|
11274
11275
|
}
|
|
11275
11276
|
},
|
|
11276
11277
|
interruptGoal: interruptRoutineGroupGoal,
|
|
11277
|
-
onRunChanged: syncRoutineRunToSource,
|
|
11278
|
+
onRunChanged: (run) => { syncRoutineRunToSource(run); routineMochiiSync.poke(); },
|
|
11278
11279
|
onRunFailed: (run) => {
|
|
11279
11280
|
if (run.threadId) {
|
|
11280
11281
|
pendingDelegationWakes.delete(run.threadId);
|
|
@@ -25523,7 +25524,7 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
25523
25524
|
() => setupLogins.stopAll(),
|
|
25524
25525
|
() => flushAllMemoryJournals(),
|
|
25525
25526
|
// the outbox is already on disk; this only stops its retry timer
|
|
25526
|
-
() => nexusMemory.stop(),
|
|
25527
|
+
() => { nexusMemory.stop(); routineMochiiSync.stop(); },
|
|
25527
25528
|
() => flushUsageLedger(DATA_DIR),
|
|
25528
25529
|
() => flushDecisionLog(DATA_DIR),
|
|
25529
25530
|
() => flushAdminActivity(DATA_DIR),
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Mirrors each routine into one private Mochii task: its schedule and next
|
|
2
|
+
// run on the task, and every finished run as a comment. Off unless the
|
|
3
|
+
// install sets features.routineMochiiSync, and it needs the configured
|
|
4
|
+
// `mochii` MCP server, which acts as the signed-in person, so each person's
|
|
5
|
+
// routines land in their own Mochii as their own private tasks.
|
|
6
|
+
//
|
|
7
|
+
// Fail closed: a new task is first created with a content-free title. Only
|
|
8
|
+
// when Mochii answers that it is private and keyed to this routine (the
|
|
9
|
+
// create contract: is_private, idempotency_key, replayed) does routine
|
|
10
|
+
// content follow. An older Mochii that ignores the contract gets its stray
|
|
11
|
+
// task deleted, and sync pauses for a few hours before checking again.
|
|
12
|
+
//
|
|
13
|
+
// No outbox: the routines and their run receipts are already on disk, so each
|
|
14
|
+
// pass compares them with what Mochii has confirmed (routine-mochii-sync.json)
|
|
15
|
+
// and sends the difference. A failed pass is simply retried on the next one.
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
19
|
+
import { customMcpServers } from "./config.js";
|
|
20
|
+
import { nexusClientFor, NexusUnavailableError } from "./nexus-memory/client.js";
|
|
21
|
+
import { scheduleText } from "./routine-requests.js";
|
|
22
|
+
const PASS_EVERY_MS = 5 * 60_000;
|
|
23
|
+
const POKE_DELAY_MS = 3_000;
|
|
24
|
+
const BLOCK_MS = 6 * 60 * 60_000;
|
|
25
|
+
/** Runs already finished when a routine is first mirrored: only the latest few become comments. */
|
|
26
|
+
const BACKFILL_RUNS = 5;
|
|
27
|
+
const MAX_COMMENT_CHARS = 4_000;
|
|
28
|
+
const FINISHED = new Set(["completed", "failed", "cancelled", "missed"]);
|
|
29
|
+
const PROBE_TITLE = "Atomic routine";
|
|
30
|
+
export function routineMochiiSyncEnabled(cfg) {
|
|
31
|
+
return cfg.features?.routineMochiiSync === true && Boolean(customMcpServers(cfg).mochii);
|
|
32
|
+
}
|
|
33
|
+
export const idempotencyKey = (routineId) => `atomic-routine:${routineId}`;
|
|
34
|
+
function record(value) {
|
|
35
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
36
|
+
}
|
|
37
|
+
export function taskTitle(routine) {
|
|
38
|
+
return `Routine: ${routine.name}${routine.enabled ? "" : " (paused)"}`;
|
|
39
|
+
}
|
|
40
|
+
export function taskDescription(routine, bot, timeZone) {
|
|
41
|
+
const next = routine.enabled && routine.nextRunAt != null ? new Date(routine.nextRunAt).toISOString() : "none";
|
|
42
|
+
return [
|
|
43
|
+
"Managed by Atomic. Edits made here are replaced on the next sync.",
|
|
44
|
+
"",
|
|
45
|
+
`Bot: ${bot ?? routine.botId}`,
|
|
46
|
+
`Schedule: ${scheduleText(routine.schedule, timeZone)}`,
|
|
47
|
+
`Status: ${routine.enabled ? "active" : "paused"}`,
|
|
48
|
+
`Next run: ${next}`,
|
|
49
|
+
"",
|
|
50
|
+
"Prompt:",
|
|
51
|
+
routine.prompt,
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|
|
54
|
+
export function runComment(run) {
|
|
55
|
+
const when = new Date(run.finishedAt ?? run.scheduledFor).toISOString();
|
|
56
|
+
const body = (run.status === "failed" ? run.error ?? run.output : run.output ?? run.error)?.trim();
|
|
57
|
+
const clipped = body && body.length > MAX_COMMENT_CHARS ? `${body.slice(0, MAX_COMMENT_CHARS)}\n\n(cut short; the full report is in Atomic)` : body;
|
|
58
|
+
return `Run ${run.status} · ${when}${run.manual ? " · started by hand" : ""}${clipped ? `\n\n${clipped}` : ""}`;
|
|
59
|
+
}
|
|
60
|
+
export class RoutineMochiiSync {
|
|
61
|
+
path;
|
|
62
|
+
now;
|
|
63
|
+
state;
|
|
64
|
+
client = null;
|
|
65
|
+
clientKey = "";
|
|
66
|
+
running = null;
|
|
67
|
+
poked;
|
|
68
|
+
timer;
|
|
69
|
+
deps;
|
|
70
|
+
constructor(deps) {
|
|
71
|
+
this.deps = deps;
|
|
72
|
+
this.path = join(deps.dataDir, "routine-mochii-sync.json");
|
|
73
|
+
this.now = deps.now ?? Date.now;
|
|
74
|
+
this.state = this.load();
|
|
75
|
+
}
|
|
76
|
+
static start(deps) {
|
|
77
|
+
const sync = new RoutineMochiiSync(deps);
|
|
78
|
+
sync.start();
|
|
79
|
+
return sync;
|
|
80
|
+
}
|
|
81
|
+
start() {
|
|
82
|
+
this.timer ??= setInterval(() => void this.pass(), PASS_EVERY_MS);
|
|
83
|
+
this.timer.unref?.();
|
|
84
|
+
this.poke();
|
|
85
|
+
}
|
|
86
|
+
stop() {
|
|
87
|
+
clearInterval(this.timer);
|
|
88
|
+
clearTimeout(this.poked);
|
|
89
|
+
this.timer = undefined;
|
|
90
|
+
void this.client?.close();
|
|
91
|
+
this.client = null;
|
|
92
|
+
}
|
|
93
|
+
/** A run changed: sync soon rather than waiting for the next pass. */
|
|
94
|
+
poke() {
|
|
95
|
+
if (this.poked || !routineMochiiSyncEnabled(this.deps.config()))
|
|
96
|
+
return;
|
|
97
|
+
this.poked = setTimeout(() => {
|
|
98
|
+
this.poked = undefined;
|
|
99
|
+
void this.pass();
|
|
100
|
+
}, POKE_DELAY_MS);
|
|
101
|
+
this.poked.unref?.();
|
|
102
|
+
}
|
|
103
|
+
status() {
|
|
104
|
+
return {
|
|
105
|
+
enabled: routineMochiiSyncEnabled(this.deps.config()),
|
|
106
|
+
mirrored: Object.keys(this.state.tasks).length,
|
|
107
|
+
blockedUntil: this.state.blockedUntil ?? null,
|
|
108
|
+
blockedReason: this.state.blockedReason ?? null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** One reconcile pass; concurrent callers share it. */
|
|
112
|
+
pass() {
|
|
113
|
+
this.running ??= this.reconcile()
|
|
114
|
+
.catch((error) => this.log(`pass stopped: ${error instanceof Error ? error.message : String(error)}`))
|
|
115
|
+
.finally(() => { this.running = null; });
|
|
116
|
+
return this.running;
|
|
117
|
+
}
|
|
118
|
+
async reconcile() {
|
|
119
|
+
const cfg = this.deps.config();
|
|
120
|
+
if (!routineMochiiSyncEnabled(cfg))
|
|
121
|
+
return;
|
|
122
|
+
if (this.state.blockedUntil && this.now() < this.state.blockedUntil)
|
|
123
|
+
return;
|
|
124
|
+
const client = this.clientFor(cfg);
|
|
125
|
+
if (!client)
|
|
126
|
+
return;
|
|
127
|
+
const routines = this.deps.listRoutines();
|
|
128
|
+
const runs = this.deps.listRuns();
|
|
129
|
+
for (const routine of routines) {
|
|
130
|
+
const mine = runs.filter((run) => run.routineId === routine.id && FINISHED.has(run.status));
|
|
131
|
+
try {
|
|
132
|
+
if (!(await this.mirror(client, routine, mine)))
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof NexusUnavailableError)
|
|
137
|
+
throw error;
|
|
138
|
+
this.log(`routine ${routine.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
// The task is gone from Mochii: forget it so the next pass creates it again.
|
|
140
|
+
if (/not found|404/i.test(String(error)))
|
|
141
|
+
this.forget(routine.id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const live = new Set(routines.map((routine) => routine.id));
|
|
145
|
+
for (const [routineId, mirrored] of Object.entries(this.state.tasks)) {
|
|
146
|
+
if (live.has(routineId))
|
|
147
|
+
continue;
|
|
148
|
+
try {
|
|
149
|
+
await client.call("update_task", { id: mirrored.taskId, status: "cancelled" });
|
|
150
|
+
await client.call("add_comment", { task_id: mirrored.taskId, content: "This routine was deleted in Atomic." });
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error instanceof NexusUnavailableError)
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
this.forget(routineId);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** False when Mochii failed the privacy check: stop the whole pass. */
|
|
160
|
+
async mirror(client, routine, finished) {
|
|
161
|
+
let mirrored = this.state.tasks[routine.id];
|
|
162
|
+
if (!mirrored) {
|
|
163
|
+
const taskId = await this.createPrivate(client, routine.id);
|
|
164
|
+
if (!taskId)
|
|
165
|
+
return false;
|
|
166
|
+
const byAge = [...finished].sort((a, b) => (a.finishedAt ?? a.scheduledFor) - (b.finishedAt ?? b.scheduledFor));
|
|
167
|
+
mirrored = { taskId, fingerprint: "", runs: byAge.slice(0, -BACKFILL_RUNS).map((run) => run.id) };
|
|
168
|
+
this.state.tasks[routine.id] = mirrored;
|
|
169
|
+
this.save();
|
|
170
|
+
}
|
|
171
|
+
const title = taskTitle(routine);
|
|
172
|
+
const description = taskDescription(routine, this.deps.botName(routine.botId), this.deps.timeZone());
|
|
173
|
+
const due = routine.enabled && routine.nextRunAt != null ? new Date(routine.nextRunAt).toISOString() : undefined;
|
|
174
|
+
const fingerprint = JSON.stringify([title, description, due]);
|
|
175
|
+
if (fingerprint !== mirrored.fingerprint) {
|
|
176
|
+
await client.call("update_task", { id: mirrored.taskId, title, description, status: "pending", ...(due ? { due_date: due } : {}) });
|
|
177
|
+
mirrored.fingerprint = fingerprint;
|
|
178
|
+
this.save();
|
|
179
|
+
}
|
|
180
|
+
const posted = new Set(mirrored.runs);
|
|
181
|
+
const fresh = finished
|
|
182
|
+
.filter((run) => !posted.has(run.id))
|
|
183
|
+
.sort((a, b) => (a.finishedAt ?? a.scheduledFor) - (b.finishedAt ?? b.scheduledFor));
|
|
184
|
+
for (const run of fresh) {
|
|
185
|
+
await client.call("add_comment", { task_id: mirrored.taskId, content: runComment(run) });
|
|
186
|
+
mirrored.runs.push(run.id);
|
|
187
|
+
this.save();
|
|
188
|
+
}
|
|
189
|
+
const kept = new Set(finished.map((run) => run.id));
|
|
190
|
+
if (mirrored.runs.some((id) => !kept.has(id))) {
|
|
191
|
+
mirrored.runs = mirrored.runs.filter((id) => kept.has(id));
|
|
192
|
+
this.save();
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
/** Creates (or, after a crash, gets back) the routine's task with a content-free title. */
|
|
197
|
+
async createPrivate(client, routineId) {
|
|
198
|
+
const key = idempotencyKey(routineId);
|
|
199
|
+
const body = await client.call("create_task", { title: PROBE_TITLE, is_private: true, idempotency_key: key });
|
|
200
|
+
const task = record(body.data);
|
|
201
|
+
const taskId = typeof task?.id === "string" ? task.id : null;
|
|
202
|
+
if (taskId && task?.is_private === true && task.idempotency_key === key && typeof body.replayed === "boolean")
|
|
203
|
+
return taskId;
|
|
204
|
+
if (taskId && task?.is_private !== true) {
|
|
205
|
+
await client.call("delete_task", { id: taskId }).catch((error) => this.log(`could not remove a non-private task ${taskId}: ${String(error)}`));
|
|
206
|
+
}
|
|
207
|
+
this.state.blockedUntil = this.now() + BLOCK_MS;
|
|
208
|
+
this.state.blockedReason = "Mochii does not support private routine tasks yet";
|
|
209
|
+
this.save();
|
|
210
|
+
this.log(`${this.state.blockedReason}; nothing was sent, checking again later`);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
forget(routineId) {
|
|
214
|
+
delete this.state.tasks[routineId];
|
|
215
|
+
this.save();
|
|
216
|
+
}
|
|
217
|
+
clientFor(cfg) {
|
|
218
|
+
if (this.deps.clientFor)
|
|
219
|
+
return (this.client ??= this.deps.clientFor(cfg));
|
|
220
|
+
const spec = customMcpServers(cfg).mochii;
|
|
221
|
+
const key = JSON.stringify(spec ?? null);
|
|
222
|
+
if (key !== this.clientKey) {
|
|
223
|
+
void this.client?.close();
|
|
224
|
+
this.client = nexusClientFor(spec);
|
|
225
|
+
this.clientKey = key;
|
|
226
|
+
}
|
|
227
|
+
return this.client;
|
|
228
|
+
}
|
|
229
|
+
load() {
|
|
230
|
+
try {
|
|
231
|
+
if (existsSync(this.path)) {
|
|
232
|
+
const parsed = record(JSON.parse(readFileSync(this.path, "utf8")));
|
|
233
|
+
if (parsed && record(parsed.tasks))
|
|
234
|
+
return parsed;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
this.log(`ignoring unreadable ${this.path}: ${String(error)}`);
|
|
239
|
+
}
|
|
240
|
+
return { tasks: {} };
|
|
241
|
+
}
|
|
242
|
+
save() {
|
|
243
|
+
if (this.state.blockedUntil && this.now() >= this.state.blockedUntil) {
|
|
244
|
+
delete this.state.blockedUntil;
|
|
245
|
+
delete this.state.blockedReason;
|
|
246
|
+
}
|
|
247
|
+
writeFileAtomic(this.path, JSON.stringify(this.state, null, 2));
|
|
248
|
+
}
|
|
249
|
+
log(message) {
|
|
250
|
+
(this.deps.log ?? ((line) => console.warn(`[routine-mochii-sync] ${line}`)))(message);
|
|
251
|
+
}
|
|
252
|
+
}
|