@chibitek/chibi 0.1.155 → 0.1.157
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-B8LdsCr1.js → abnfDiagram-O67JEVCF-BkYbog6D.js} +1 -1
- package/dist/assets/{arc-DU5fSmKU.js → arc-QwTuMsxp.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-DdQSIFKv.js → architectureDiagram-NJMV4G6O-GdicO2PQ.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CaPcGYSS.js → blockDiagram-BEXU5L5S-Dkr29TFF.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-unt0ePgO.js → c4Diagram-YGBWAQC7-Dz_3dstx.js} +1 -1
- package/dist/assets/channel-D2y0i2ps.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Babg5Fzu.js → chunk-2Q5K7J3B-CGsfu-PI.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D1-dWVHo.js → chunk-3FUC2YCW-CDqI37_6.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DU7nSMbT.js → chunk-5VM5RSS4-DnmSuyTb.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-QIjCJIhV.js → chunk-GTNCS2PH-Dh7tXcJT.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-pSByUE5R.js → chunk-GWA4HPMP-D_SM1ZrY.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-Dj2AaR1b.js → chunk-JWPE2WC7-DmeMuTsI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-pZbN0-FP.js → chunk-NETBCI7D-R1UT9b5f.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-Bqarwe4D.js → chunk-WEXAMYUT-DpPfU1yO.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-WV8lm3aa.js → chunk-XXDRQBXY-DlihfTkM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BANBtky1.js → classDiagram-v2-NBCMYWYE-BlTV6lSC.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CtrktgBC.js → cose-bilkent-JH36ORCC-Br1kPFWw.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-DnhVVfba.js → cynefin-OW5HDTMX-BqPp3ZMd.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-B42SRR6x.js → cynefinDiagram-VND7K2PF-DFfs_k8-.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DM--vFMK.js → dagre-6A5THRUB-Ch3xIJA7.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-ZBupBrPU.js → diagram-22UHCM2B-DIHUD87N.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-BPQzNIOd.js → diagram-3UASUU5V-CAgQQdeF.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-5kO8vf8Z.js → diagram-ATOU4E4O-9HVKodrI.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DtxogB86.js → diagram-CDSNMT55-DU2kDq_h.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Co9-nYw_.js → diagram-MLGK6HIB-D-qTAhHD.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-DWRuMh5y.js → diagram-MPIPVDR6-C31SnI9P.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DU_nmoG9.js → ebnfDiagram-ZINNZB2B-BHrdiBVd.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-Bcje7S94.js → elk-276RUBZZ-DWyljRo5.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-9aCo_2CI.js → erDiagram-OPXOYQCR-DyY59XPp.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-D4FYFyc5.js → flowDiagram-KWPJA3E3-BevMIeAy.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-WQHX5rbx.js → ganttDiagram-FUAMR5RP-Ct18WHf4.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DRyzBdiV.js → gitGraphDiagram-X574FWY7-yhixO1N3.js} +1 -1
- package/dist/assets/{index-CG1OvRgO.js → index-BEJ8EKQt.js} +1 -1
- package/dist/assets/index-DGWeJcck.css +1 -0
- package/dist/assets/{index-D_8SD_0K.js → index-ziyH_uer.js} +170 -168
- package/dist/assets/{infoDiagram-VRGFBTTK-D7SOmp4n.js → infoDiagram-VRGFBTTK-CYa-r_S5.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CdxOnDVX.js → ishikawaDiagram-OU5B5YK6-CnxwM7gY.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CH7477Oa.js → journeyDiagram-ZHPQQLJL-RKQfXBrp.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BdpVvkHd.js → kanban-definition-PNTS6WVX-CSlRDIF6.js} +1 -1
- package/dist/assets/{linear-DTGq6qn5.js → linear-Ch19KVyt.js} +1 -1
- package/dist/assets/{mermaid.core-Sn2sv0Ti.js → mermaid.core-Bek7I_t6.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-koCmkYuq.js → mindmap-definition-NLK3R4M7-B_LiP8su.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-C3sqhm8e.js → pegDiagram-GJSIUBJH-DhzQeiZf.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-BELBK122.js → pieDiagram-5QR66LMP-D-BYNKHS.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Cksi5LSm.js → quadrantDiagram-O4NWA36T-DnCm0heP.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-f1av_EZW.js → railroadDiagram-XR7U4H2S-COqgYnhn.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CW4O5nER.js → requirementDiagram-PLB6GJNP-Dvt72ajf.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-Dz8ADHNY.js → sankeyDiagram-IPEJSGJF-CisC9nbp.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-Cc3HE0iZ.js → sequenceDiagram-PO4LG4MO-BO8xqvxa.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-gdaZNHnK.js → sizeCapture-INFHLROL-DLk1jBzf.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK--u8LCnab.js → stateDiagram-v2-GCMORJYK-C_wutSE7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-4MAw4kdO.js → swimlanes-2SLR337P-hqo1UlEi.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D15JqTL9.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-CL89YkCT.js → timeline-definition-EJHVYXUP-C_DJldPg.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-C92l7YQe.js → usecaseDiagram-POWQR4AR-CsidVXMA.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-17siYhSj.js → vennDiagram-UO4OBE2U-Bs-IIqn_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-CNwa9VmN.js → wardleyDiagram-VNRHLVJA-CXEUyxaj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Dnu6Xzih.js → xychartDiagram-PMCCYNJV-B9qvmm4P.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +81 -81
- package/dist-server/drivers/agents-proxy.js +81 -81
- package/dist-server/index.js +2354 -1729
- package/dist-server/openmausbot.js +676 -643
- package/dist-server/pair-cli.js +676 -643
- package/dist-server/server/app-installers.js +107 -7
- package/dist-server/server/cli-help.js +74 -0
- package/dist-server/server/cli.js +32 -74
- package/dist-server/server/device-routing.js +60 -0
- package/dist-server/server/drivers/claude.js +13 -11
- package/dist-server/server/drivers/local-inject.js +3 -4
- package/dist-server/server/engine-tiers.js +5 -14
- package/dist-server/server/index.js +84 -104
- package/dist-server/server/personal-goal-runtime.js +86 -0
- package/dist-server/server/personal-goal-turn.js +125 -0
- package/dist-server/server/personal-goals.js +195 -0
- package/dist-server/server/routes/local-models.js +3 -2
- package/dist-server/server/skill-fetch.js +14 -0
- package/dist-server/shared/engine-choices.js +15 -0
- package/dist-server/shared/engine-tiers.js +74 -2
- package/dist-server/shared/local-models.js +1 -3
- package/dist-server/shared/personal-goal.js +43 -0
- package/dist-server/vps-container-mcp.js +81 -81
- package/package.json +1 -1
- package/dist/assets/channel-GBaPOaz-.js +0 -1
- package/dist/assets/index-C8YP7Zxj.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CamSa0NE.js +0 -8
- package/dist-server/connector-proxy.js +0 -205
- package/dist-server/server/composio.js +0 -1051
- package/dist-server/server/connector-proxy.js +0 -249
- package/dist-server/server/mcp-public-registry.js +0 -325
- package/dist-server/shared/connector-availability.js +0 -12
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { hasUnboundGoalGuard, goalGuardForEvent, addGoalGuard, removeGoalGuard, runPersonalGoalTurn, updatePersonalGoalReceipt } from "./personal-goal-turn.js";
|
|
2
|
+
import { PersonalGoalRuntime, receiveGoalCommand } from "./personal-goal-runtime.js";
|
|
3
|
+
import { personalGoalInstructions } from "./personal-goals.js";
|
|
4
|
+
import { parseGoalCommand } from "../shared/personal-goal.js";
|
|
1
5
|
// OpenMausBot server — the harness host. Clients hold no transports
|
|
2
6
|
// (upstream rule): the React app dispatches typed commands over HTTP and
|
|
3
7
|
// folds one SSE event stream; every provider process runs here.
|
|
@@ -143,7 +147,7 @@ import { MEMORY_INDEX, MemoryStoreError, memoryCapacity, memoryOverview, openFol
|
|
|
143
147
|
import { beginMemoryTurn, endMemoryTurn, flushAllMemoryJournals, flushMemoryJournal, journalMemoryDelete, journalMemoryWrite, readMemoryJournal, revertMemoryChange, } from "./memory-journal.js";
|
|
144
148
|
import { readSectionContext, readSections, sectionContextKey, sectionContextLabel, sectionContextSystemPrompt, writeSectionContext, SECTION_CONTEXT_MAX_BYTES, } from "./section-context.js";
|
|
145
149
|
import { applyStagedSkillWrite, applySkillWriteWithReceipt, getStagedSkillWrite, installSkill, parseSkillMd, scanSkillText, listSkills, listStagedSkillWrites, isSkillName, readSkillFile, rejectStagedSkillWrite, removeSkill, setSkillEnabled, skillsSystemPrompt, stageSkillWrite, } from "./skills.js";
|
|
146
|
-
import {
|
|
150
|
+
import { resolveSkillImport, skillImportSchema } from "./skill-fetch.js";
|
|
147
151
|
import { pickBotName } from "./names.js";
|
|
148
152
|
import { botSkillTemplateSchema, newBotDefaultsSchema, resolveBotCreationDefaults } from "./new-bot-defaults.js";
|
|
149
153
|
import { expandLearnTurnText, learnSource } from "./skill-learn.js";
|
|
@@ -235,6 +239,7 @@ import { activityCsv, activityEntries, appendAdminAction, botAuditSnapshot, botC
|
|
|
235
239
|
import { frameForMember, memberBody, memberBot, noteSeen, notFoundFor, audienceEquals, memberGroup, narrowestAudience, parseVisibility, pathSubject, roomFeeds, routineVisible, sameAudience, SEES_EVERYTHING, SEES_ALL_BUT_PRIVATE, VisibleSet, } from "./bot-visibility.js";
|
|
236
240
|
import { describeBrand, loadBrand } from "./brand.js";
|
|
237
241
|
import { deliverSseFrame } from "./sse-fanout.js";
|
|
242
|
+
import { DeviceRouteRegistry, removeStream } from "./device-routing.js";
|
|
238
243
|
import { PHONE_SECRET_PROTOCOL_VERSION, PhoneSecretBridge, PhoneSecretError, PhoneSecretSubmissionRegistry, assertPhoneSecretRequestMatches, phoneSecretOperationId, } from "./phone-secret.js";
|
|
239
244
|
// Keep these two last: a route module may import any server module, and
|
|
240
245
|
// loading the table after everything above leaves module start-up order as is.
|
|
@@ -248,7 +253,7 @@ import { createModelLevelsRoutes } from "./routes/model-levels.js";
|
|
|
248
253
|
import { createMcpMarketplaceRoutes } from "./routes/mcp-marketplace.js";
|
|
249
254
|
import { createLocalModelRoutes, OLLAMA_LOCAL_URL, ollamaInstalledHere } from "./routes/local-models.js";
|
|
250
255
|
import { createSetupRoutes } from "./routes/setup.js";
|
|
251
|
-
import { installOllamaApp, openPodmanInstaller, runTool, startPodmanMachine } from "./app-installers.js";
|
|
256
|
+
import { installOllamaApp, openPodmanInstaller, startOllamaApp, runTool, startPodmanMachine } from "./app-installers.js";
|
|
252
257
|
import { CliLoginRunner } from "./cli-login.js";
|
|
253
258
|
import { JobFailure, SetupJobs } from "./setup-jobs.js";
|
|
254
259
|
import { localHost, probeLocalInjects } from "./drivers/local-inject.js";
|
|
@@ -1820,6 +1825,7 @@ function requestedTaskBot(botId, rawThreadId) {
|
|
|
1820
1825
|
return task;
|
|
1821
1826
|
}
|
|
1822
1827
|
async function interruptDirectThread(botId, threadId) {
|
|
1828
|
+
personalGoals.steer(threadId);
|
|
1823
1829
|
const requestOwner = directRequestOwners.get(threadId);
|
|
1824
1830
|
if (requestOwner) {
|
|
1825
1831
|
requestOwner.stopped = true;
|
|
@@ -3350,7 +3356,7 @@ function localModeTurn(resolution, turn) {
|
|
|
3350
3356
|
return { ...resolution, local: true, choice: { ...choice, selection: placement.selection, candidate: placement.candidate } };
|
|
3351
3357
|
}
|
|
3352
3358
|
const payPerUsePausedLine = (until) => `Pay-per-use is paused until ${restTime(until)}, when a subscription is back, so this turn stopped.`;
|
|
3353
|
-
/** A model this machine serves:
|
|
3359
|
+
/** A model this machine serves: a rung when a ladder names it, else local rules only. */
|
|
3354
3360
|
const isLocalModel = (candidate, model) => localModel(candidate, model) || modelLevels.classOf(candidate.driverKind, model)?.local === true;
|
|
3355
3361
|
// ── silent automatic failover (docs/chibitek/engine-tiers.md, step 2) ────
|
|
3356
3362
|
// Resting engines are shared by every bot and survive a restart.
|
|
@@ -3589,10 +3595,7 @@ const eventBase = (event) => ({
|
|
|
3589
3595
|
...(event.providerInstanceId ? { providerInstanceId: event.providerInstanceId } : {}), ...(event.turnId ? { turnId: event.turnId } : {}),
|
|
3590
3596
|
});
|
|
3591
3597
|
const restTime = (at) => new Date(at).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
3592
|
-
/**
|
|
3593
|
-
* out. Rest the engine, then run the same turn on the next engine at the
|
|
3594
|
-
* same level, silently; or, when nothing is left, stop it with one line
|
|
3595
|
-
* naming the resting engines. Returns what subscribers see. */
|
|
3598
|
+
/** Rest a refused engine and continue on an eligible engine, or show an actionable line. */
|
|
3596
3599
|
function engineFailover(turn, completion) {
|
|
3597
3600
|
const failure = turn.failure;
|
|
3598
3601
|
const record = failoverTurns.get(turn.threadId);
|
|
@@ -3615,17 +3618,14 @@ function engineFailover(turn, completion) {
|
|
|
3615
3618
|
const restFailed = () => engineRests.rest(turn.instanceId, failedCandidate ? engineAccount(failedCandidate) : undefined, failure.kind, { resetAt: failure.resetAt, resetText: failure.resetText, windowMs: failure.windowMs, message: redactSecretsInText(failure.message),
|
|
3616
3619
|
...(failure.permanent ? { permanent: true } : {}), ...(scope ? { scope } : {}), ...(ran?.model ? { model: ran.model } : {}),
|
|
3617
3620
|
...(failure.kind === "auth_failed" && failedCandidate ? { reportedSignedIn: failedCandidate.snapshot.state === "available" && failedCandidate.snapshot.authenticated === true } : {}) });
|
|
3618
|
-
//
|
|
3619
|
-
// next engine: the engine rests (the circuit breaker) only once the turn
|
|
3620
|
-
// really moves, and with nowhere to go the turn ends with the engine's own
|
|
3621
|
-
// report, exactly as before, rather than a "resting" line about it.
|
|
3621
|
+
// Unknown failures rest only when the turn moves; otherwise preserve their report.
|
|
3622
3622
|
const rest = failure.kind === "provider_error" ? undefined : restFailed();
|
|
3623
3623
|
const label = (instanceId) => candidates.find((candidate) => candidate.instanceId === instanceId)?.displayName ?? instanceId;
|
|
3624
3624
|
// The one plain line, never the provider's own words: which account needs
|
|
3625
3625
|
// the person and where to fix it, or that every engine is resting.
|
|
3626
3626
|
const stop = (line, category = "failover_exhausted") => [
|
|
3627
3627
|
{ ...eventBase(completion), type: "runtime.error", message: line, terminal: true, category, ...(failure.setup ? { setup: true } : {}) },
|
|
3628
|
-
completion,
|
|
3628
|
+
failure.kind === "auth_failed" ? { ...completion, stopReason: "auth_required" } : completion,
|
|
3629
3629
|
];
|
|
3630
3630
|
const restingNamed = (instanceIds) => [...new Set(instanceIds)].flatMap((instanceId) => {
|
|
3631
3631
|
const candidate = candidates.find((entry) => entry.instanceId === instanceId);
|
|
@@ -3648,13 +3648,11 @@ function engineFailover(turn, completion) {
|
|
|
3648
3648
|
return stop(pick.line, pick.category);
|
|
3649
3649
|
const { choice } = pick;
|
|
3650
3650
|
const tried = new Set(record.tried);
|
|
3651
|
-
//
|
|
3652
|
-
// chip, no notification, no incident, and nothing settled that the same
|
|
3653
|
-
// turn still owns.
|
|
3651
|
+
// Retire the failed attempt without settling the request that the next engine owns.
|
|
3654
3652
|
const moved = ran ?? record.selection;
|
|
3655
3653
|
const failedOwner = turnResourceOwners.get(turn.threadId);
|
|
3656
3654
|
const tokens = completion.usage ?? turnUsage.get(turn.threadId);
|
|
3657
|
-
const partialText = turn.partial.trim();
|
|
3655
|
+
const partialText = groupGoalCoordinatorTurns.get(turn.threadId)?.size ? parseGroupGoalDecision(turn.partial).visibleText : turn.partial.trim();
|
|
3658
3656
|
const requestOwner = directRequestOwners.get(turn.threadId);
|
|
3659
3657
|
if (partialText) {
|
|
3660
3658
|
// The answer it was in the middle of stays: the person saw it stream.
|
|
@@ -3666,6 +3664,12 @@ function engineFailover(turn, completion) {
|
|
|
3666
3664
|
const tools = earlier.filter((message) => message.kind === "activity" && message.tool?.name).map((message) => message.tool.name);
|
|
3667
3665
|
const note = continuationNote(said.map((message) => message.text ?? ""), tools);
|
|
3668
3666
|
const attempt = record.attempt + 1;
|
|
3667
|
+
for (const guard of groupGoalCoordinatorTurns.get(turn.threadId) ?? []) {
|
|
3668
|
+
if (!guard.discard && (guard.turnId === completion.turnId || guard.turnId === turn.turnId)) {
|
|
3669
|
+
guard.turnId = undefined;
|
|
3670
|
+
guard.assistantItems = [];
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3669
3673
|
const rerun = startTurn(turn.botId, note ? `${record.text}\n\n${note}` : record.text, {
|
|
3670
3674
|
...record.opts,
|
|
3671
3675
|
threadId: turn.threadId,
|
|
@@ -3703,6 +3707,11 @@ function engineFailover(turn, completion) {
|
|
|
3703
3707
|
});
|
|
3704
3708
|
}
|
|
3705
3709
|
retireFailedAttempt(turn.botId, turn.threadId, turn.generation, failedOwner, completion);
|
|
3710
|
+
if (failure.kind === "auth_failed")
|
|
3711
|
+
reportIncident({
|
|
3712
|
+
kind: "engine-fallback", bot: profile, threadId: turn.threadId, notifyChief: true,
|
|
3713
|
+
detail: `${engineLabel(candidates, moved)} needs signing in again in Settings > Engines; the turn continued on ${engineLabel(candidates, choice.selection)}`,
|
|
3714
|
+
});
|
|
3706
3715
|
console.error(`[omb-failover] thread=${turn.threadId} ${failure.kind}: ${moved.instanceId} -> ${choice.selection.instanceId} (attempt ${attempt})`);
|
|
3707
3716
|
// No turnId: the failed provider turn is retired, and its id would hide
|
|
3708
3717
|
// this record from every window.
|
|
@@ -5013,67 +5022,21 @@ const publicBotQueuedMessages = () => queuedSteerSnapshot((botId, threadId) => B
|
|
|
5013
5022
|
const groupTurnOperations = new Map();
|
|
5014
5023
|
const groupGoalCoordinatorTurns = new Map();
|
|
5015
5024
|
const GROUP_GOAL_COORDINATOR_GUARD_MS = 5 * 60_000;
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
.some((turn) => turn.discard && !turn.turnId);
|
|
5032
|
-
}
|
|
5033
|
-
/** Match private coordinator output to one provider turn, never merely to a
|
|
5034
|
-
* reusable room thread. Most adapters emit turn.started before sendTurn
|
|
5035
|
-
* resolves, so the first stable event may bind an otherwise pending guard. */
|
|
5036
|
-
function groupGoalCoordinatorTurnForEvent(event) {
|
|
5037
|
-
const turns = groupGoalCoordinatorTurns.get(event.threadId);
|
|
5038
|
-
if (!turns?.size)
|
|
5039
|
-
return undefined;
|
|
5040
|
-
const candidates = [...turns];
|
|
5041
|
-
if (event.turnId) {
|
|
5042
|
-
const exact = candidates.find((turn) => turn.turnId === event.turnId);
|
|
5043
|
-
if (exact)
|
|
5044
|
-
return exact;
|
|
5045
|
-
// Until an interrupted handshake returns its own id, no new id can be
|
|
5046
|
-
// attributed safely. The stall fallback keeps this thread unavailable in
|
|
5047
|
-
// that narrow window; private text is suppressed below until sendTurn's
|
|
5048
|
-
// result binds the old guard or its bounded expiry releases ownership.
|
|
5049
|
-
const unboundDiscarded = candidates.filter((turn) => turn.discard && !turn.turnId);
|
|
5050
|
-
if (unboundDiscarded.length > 0) {
|
|
5051
|
-
// The ownership fallback below keeps a lone abandoned handshake's
|
|
5052
|
-
// thread closed, so its first eventual id can safely bind here. More
|
|
5053
|
-
// than one unbound candidate is genuinely ambiguous and stays gated.
|
|
5054
|
-
if (candidates.length === 1) {
|
|
5055
|
-
unboundDiscarded[0].turnId = event.turnId;
|
|
5056
|
-
// This event is the first stable identity for an already-abandoned
|
|
5057
|
-
// provider turn. Tombstone it immediately so this event and every
|
|
5058
|
-
// later completion/request cannot settle a replacement on the same
|
|
5059
|
-
// room thread.
|
|
5060
|
-
retireProviderTurn(event.turnId);
|
|
5061
|
-
return unboundDiscarded[0];
|
|
5062
|
-
}
|
|
5063
|
-
return undefined;
|
|
5064
|
-
}
|
|
5065
|
-
const pending = candidates.findLast((turn) => !turn.turnId && !turn.discard);
|
|
5066
|
-
if (pending && !pending.turnId) {
|
|
5067
|
-
pending.turnId = event.turnId;
|
|
5068
|
-
return pending;
|
|
5069
|
-
}
|
|
5070
|
-
return undefined;
|
|
5071
|
-
}
|
|
5072
|
-
// Turn-scoped events normally carry an id. If an adapter omits it, fail
|
|
5073
|
-
// closed for private text; with multiple overlapping guards there is no
|
|
5074
|
-
// safe way to attribute a completion, so leave cleanup to the bounded timer.
|
|
5075
|
-
return candidates.length === 1 ? candidates[0] : undefined;
|
|
5076
|
-
}
|
|
5025
|
+
const personalGoals = new PersonalGoalRuntime(join(DATA_DIR, "personal-goals.json"), {
|
|
5026
|
+
busy: (botId, threadId) => threadBusy(botId, threadId) || Boolean(activeGroupTurnForBot(botId)),
|
|
5027
|
+
interrupt: async (botId, threadId, goalId) => {
|
|
5028
|
+
const owns = [...(groupGoalCoordinatorTurns.get(threadId) ?? [])].some(guard => guard.personalGoalId === goalId && !guard.discard && (!guard.turnId || guard.turnId === liveTurnByThread.get(threadId)));
|
|
5029
|
+
if (owns)
|
|
5030
|
+
await interruptDirectThread(botId, threadId);
|
|
5031
|
+
},
|
|
5032
|
+
changed: (goal) => updatePersonalGoalReceipt(goal, store),
|
|
5033
|
+
run: (goal) => runPersonalGoalTurn(goal, { store, addGuard: addGroupGoalCoordinatorTurn, removeGuard: removeGroupGoalCoordinatorTurn,
|
|
5034
|
+
subscribe: (listener) => bus.subscribe(listener), ignore: shouldIgnoreProviderEvent, busy: threadBusy, start: startTurn }),
|
|
5035
|
+
});
|
|
5036
|
+
const addGroupGoalCoordinatorTurn = (threadId, turn) => addGoalGuard(groupGoalCoordinatorTurns, threadId, turn);
|
|
5037
|
+
const removeGroupGoalCoordinatorTurn = (threadId, turn) => removeGoalGuard(groupGoalCoordinatorTurns, threadId, turn);
|
|
5038
|
+
const hasUnboundDiscardedGroupGoalTurn = (threadId) => hasUnboundGoalGuard(groupGoalCoordinatorTurns, threadId);
|
|
5039
|
+
const groupGoalCoordinatorTurnForEvent = (event) => goalGuardForEvent(groupGoalCoordinatorTurns, retireProviderTurn, event);
|
|
5077
5040
|
function groupIsWorking(group) {
|
|
5078
5041
|
return Boolean(group.busyBotId) || Boolean(groupTurnOperations.get(group.id)?.size);
|
|
5079
5042
|
}
|
|
@@ -6024,6 +5987,9 @@ function messageWindow(threadId, messageId, limit) {
|
|
|
6024
5987
|
return { messages: all.slice(start, stop).map(slimMessage), hasMore: start > 0 };
|
|
6025
5988
|
}
|
|
6026
5989
|
const sseClients = new Set();
|
|
5990
|
+
// Approval device routing: docs/specs/remote-approval/spec.md.
|
|
5991
|
+
const deviceRoutes = new DeviceRouteRegistry();
|
|
5992
|
+
const removeSseClient = (client) => removeStream(sseClients, deviceRoutes, client);
|
|
6027
5993
|
/** The frame at which a bot's audience last changed (see the SSE route). */
|
|
6028
5994
|
let audienceChangedAt = 0;
|
|
6029
5995
|
/** A bot's audience changed: end every member stream. Each reconnects at
|
|
@@ -6035,27 +6001,15 @@ function audienceChanged() {
|
|
|
6035
6001
|
for (const client of sseClients) {
|
|
6036
6002
|
if (client.viewer.kind !== "member")
|
|
6037
6003
|
continue;
|
|
6038
|
-
|
|
6039
|
-
try {
|
|
6040
|
-
client.res.end();
|
|
6041
|
-
}
|
|
6042
|
-
catch {
|
|
6043
|
-
/* already gone */
|
|
6044
|
-
}
|
|
6004
|
+
removeSseClient(client);
|
|
6045
6005
|
}
|
|
6046
6006
|
}
|
|
6047
6007
|
function closeSessionStreams(sessionId) {
|
|
6048
6008
|
browserLive.closeForOwner(sessionId);
|
|
6049
|
-
for (const client of sseClients) {
|
|
6009
|
+
for (const client of Array.from(sseClients)) {
|
|
6050
6010
|
if (client.sessionId !== sessionId)
|
|
6051
6011
|
continue;
|
|
6052
|
-
|
|
6053
|
-
try {
|
|
6054
|
-
client.res.end();
|
|
6055
|
-
}
|
|
6056
|
-
catch {
|
|
6057
|
-
/* already gone */
|
|
6058
|
-
}
|
|
6012
|
+
removeSseClient(client);
|
|
6059
6013
|
}
|
|
6060
6014
|
}
|
|
6061
6015
|
sessions.onSessionRevoked((sessionId) => {
|
|
@@ -6253,7 +6207,7 @@ function broadcast(payload, options = {}) {
|
|
|
6253
6207
|
// Screen frames are replaceable and durable events are not: see
|
|
6254
6208
|
// ./sse-fanout.ts for the backpressure/bound decision this makes.
|
|
6255
6209
|
if (deliverSseFrame(client, kind, out) === "disconnected") {
|
|
6256
|
-
|
|
6210
|
+
removeSseClient(client);
|
|
6257
6211
|
}
|
|
6258
6212
|
}
|
|
6259
6213
|
}
|
|
@@ -8271,7 +8225,7 @@ bus.subscribe((event) => {
|
|
|
8271
8225
|
if (!event.ok && event.stopReason !== "interrupted" && !lazyClaimAlreadyReported && !routines?.runForThread(event.threadId)) {
|
|
8272
8226
|
const broken = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
|
|
8273
8227
|
if (broken)
|
|
8274
|
-
reportIncident({ kind: "failed", bot: broken, threadId: event.threadId, detail: failedRunDetail(event.stopReason, turnError) });
|
|
8228
|
+
reportIncident({ kind: "failed", bot: broken, threadId: event.threadId, detail: failedRunDetail(event.stopReason, turnError), notifyChief: event.stopReason === "auth_required" });
|
|
8275
8229
|
}
|
|
8276
8230
|
const lastReported = turnUsage.get(event.threadId);
|
|
8277
8231
|
turnUsage.delete(event.threadId);
|
|
@@ -8637,7 +8591,7 @@ function reportIncident(input) {
|
|
|
8637
8591
|
const task = store.taskByThread(bot.id, threadId);
|
|
8638
8592
|
// A thread another bot opened and is watching is that bot's to handle:
|
|
8639
8593
|
// the delegator is woken with the failure already (wakeDelegationSource).
|
|
8640
|
-
if (task?.openedBy?.delegationId || delegationWatch.has(threadId) || roomHandoffs.activeDirect(threadId))
|
|
8594
|
+
if (!input.notifyChief && (task?.openedBy?.delegationId || delegationWatch.has(threadId) || roomHandoffs.activeDirect(threadId)))
|
|
8641
8595
|
return;
|
|
8642
8596
|
const group = store.groupByThread(threadId);
|
|
8643
8597
|
const incident = {
|
|
@@ -10760,6 +10714,7 @@ async function startTurn(botId, text, opts) {
|
|
|
10760
10714
|
? await nexusMemory.readBack({ bot: liveBot ?? bot, threadId, query: providerText })
|
|
10761
10715
|
: "";
|
|
10762
10716
|
const prompt = buildSystemPrompt(persona, liveBot?.soul ?? bot.soul ?? "", roomPromptSections(roomTurn, [
|
|
10717
|
+
{ id: "personal-goal", label: "Active goal", text: personalGoals.goals.get(threadId)?.status === "working" ? personalGoalInstructions(personalGoals.goals.get(threadId)) : "" },
|
|
10763
10718
|
{ id: "user-profile", label: "About the user", text: userProfileSystemPrompt(cfg.profile) },
|
|
10764
10719
|
// the ICM folder is on this machine; a cloud run cannot reach it
|
|
10765
10720
|
// first after the soul: the block names agent tools, so it only goes
|
|
@@ -16129,7 +16084,7 @@ ROUTES.push(createSetupRoutes({
|
|
|
16129
16084
|
}
|
|
16130
16085
|
},
|
|
16131
16086
|
startOllama: async (context) => {
|
|
16132
|
-
|
|
16087
|
+
startOllamaApp();
|
|
16133
16088
|
for (let attempt = 0; attempt < 30 && !context.signal.aborted; attempt++) {
|
|
16134
16089
|
if ((await setupOllamaState()) === "running")
|
|
16135
16090
|
return;
|
|
@@ -19062,6 +19017,7 @@ const handleRequest = async (req, res) => {
|
|
|
19062
19017
|
admin: auth.scopes.includes("admin"),
|
|
19063
19018
|
screens: url.searchParams.get("screens") !== "off",
|
|
19064
19019
|
backpressured: false,
|
|
19020
|
+
authKind: auth.kind,
|
|
19065
19021
|
viewer,
|
|
19066
19022
|
// What this person's hydration (GET /api/bots, narrowed the same way) showed them.
|
|
19067
19023
|
seen: viewer.kind === "all" && viewer.privateOwner ? { bots: new Set(), groups: new Set() } : {
|
|
@@ -19105,6 +19061,7 @@ const handleRequest = async (req, res) => {
|
|
|
19105
19061
|
// A client that offered no cursor gets false too, which is exactly
|
|
19106
19062
|
// what a cold start should do.
|
|
19107
19063
|
resumed,
|
|
19064
|
+
approvalTarget: deviceRoutes.route(),
|
|
19108
19065
|
})}\n\n`);
|
|
19109
19066
|
if (resumed) {
|
|
19110
19067
|
for (const buffered of replayBuffer) {
|
|
@@ -19120,6 +19077,7 @@ const handleRequest = async (req, res) => {
|
|
|
19120
19077
|
}
|
|
19121
19078
|
}
|
|
19122
19079
|
sseClients.add(client);
|
|
19080
|
+
deviceRoutes.streamOpened(client.authKind);
|
|
19123
19081
|
// Keep this long-lived response out of socket idle-timeout handling
|
|
19124
19082
|
// without weakening timeouts for every other API request.
|
|
19125
19083
|
req.socket.setTimeout(0);
|
|
@@ -19139,7 +19097,7 @@ const handleRequest = async (req, res) => {
|
|
|
19139
19097
|
}, SSE_HEARTBEAT_MS);
|
|
19140
19098
|
req.on("close", () => {
|
|
19141
19099
|
clearInterval(keepalive);
|
|
19142
|
-
|
|
19100
|
+
removeSseClient(client);
|
|
19143
19101
|
});
|
|
19144
19102
|
return;
|
|
19145
19103
|
}
|
|
@@ -20584,11 +20542,11 @@ const handleRequest = async (req, res) => {
|
|
|
20584
20542
|
});
|
|
20585
20543
|
}
|
|
20586
20544
|
if (method === "POST" && path === "/api/bot-defaults/skills/preview") {
|
|
20587
|
-
|
|
20545
|
+
// Drafts accept the same uploaded bytes as existing bots, without installing them.
|
|
20546
|
+
const checked = skillImportSchema.safeParse(await readBody(req));
|
|
20588
20547
|
if (!checked.success)
|
|
20589
20548
|
return json(res, 400, { error: checked.error.message });
|
|
20590
|
-
const
|
|
20591
|
-
const fetched = await fetchSkillFromSource(input.source);
|
|
20549
|
+
const fetched = await resolveSkillImport(checked.data);
|
|
20592
20550
|
if ("error" in fetched)
|
|
20593
20551
|
return json(res, 422, { error: fetched.error });
|
|
20594
20552
|
const skills = [];
|
|
@@ -21701,10 +21659,10 @@ const handleRequest = async (req, res) => {
|
|
|
21701
21659
|
if (m && method === "POST") {
|
|
21702
21660
|
if (!store.bot(m[1]))
|
|
21703
21661
|
return json(res, 404, { error: "no such bot" });
|
|
21704
|
-
const parsed =
|
|
21662
|
+
const parsed = skillImportSchema.safeParse(await readBody(req));
|
|
21705
21663
|
if (!parsed.success)
|
|
21706
|
-
return json(res, 400, { error: "
|
|
21707
|
-
const fetched = await
|
|
21664
|
+
return json(res, 400, { error: "Choose a Markdown skill file or enter a GitHub source (maximum 256 KiB per file)." });
|
|
21665
|
+
const fetched = await resolveSkillImport(parsed.data);
|
|
21708
21666
|
if ("error" in fetched)
|
|
21709
21667
|
return json(res, 422, { error: fetched.error });
|
|
21710
21668
|
const results = fetched.skills.map((skill) => installSkill(m[1], skill.source, skill.files));
|
|
@@ -22233,10 +22191,20 @@ const handleRequest = async (req, res) => {
|
|
|
22233
22191
|
userAgent: typeof req.headers["user-agent"] === "string" ? req.headers["user-agent"] : undefined,
|
|
22234
22192
|
relayed: Boolean(onBehalfOf),
|
|
22235
22193
|
}));
|
|
22194
|
+
let goalCommand;
|
|
22195
|
+
try {
|
|
22196
|
+
goalCommand = parseGoalCommand(text);
|
|
22197
|
+
}
|
|
22198
|
+
catch (error) {
|
|
22199
|
+
return json(res, 400, { error: error instanceof Error ? error.message : "Invalid goal command" });
|
|
22200
|
+
}
|
|
22201
|
+
if (guarded && goalCommand)
|
|
22202
|
+
return json(res, 400, { error: "Goal commands require the conversation command interface." });
|
|
22236
22203
|
// The send is acknowledged before the turn starts, so a workspace at its
|
|
22237
22204
|
// spend limit is refused here, where the person can see it.
|
|
22238
22205
|
try {
|
|
22239
|
-
|
|
22206
|
+
if (!goalCommand || goalCommand.action === "start" || goalCommand.action === "resume")
|
|
22207
|
+
assertWithinBudget(cfg, DATA_DIR);
|
|
22240
22208
|
}
|
|
22241
22209
|
catch (error) {
|
|
22242
22210
|
return json(res, 409, { error: error instanceof Error ? error.message : String(error), code: "spend_cap" });
|
|
@@ -22305,6 +22273,12 @@ const handleRequest = async (req, res) => {
|
|
|
22305
22273
|
const message = await startTurn(bot.id, text, { threadId, replyTo, sendId, sender: messageSender(auth), trigger });
|
|
22306
22274
|
return { ok: true, threadId, message };
|
|
22307
22275
|
}
|
|
22276
|
+
if (goalCommand) {
|
|
22277
|
+
if (auth.kind === "assistant")
|
|
22278
|
+
throw Object.assign(new Error("Only the person controls goals."), { status: 403 });
|
|
22279
|
+
return receiveGoalCommand(personalGoals, store, bot.id, threadId, goalCommand, text, sendId, messageSender(auth), trigger);
|
|
22280
|
+
}
|
|
22281
|
+
personalGoals.steer(threadId);
|
|
22308
22282
|
// Claude can accept the message inside its live turn. If the write
|
|
22309
22283
|
// loses a race with turn settlement, or the engine cannot steer, the
|
|
22310
22284
|
// existing server-side queue records it atomically for the next turn.
|
|
@@ -23733,6 +23707,8 @@ const handleRequest = async (req, res) => {
|
|
|
23733
23707
|
if (method === "GET" && authStatus) {
|
|
23734
23708
|
res.setHeader("cache-control", "no-store");
|
|
23735
23709
|
const state = await providerAuthSessions.status(authStatus[1], auth.kind === "session" ? auth.session.id : "loopback", url.searchParams.get("flowId") ?? "");
|
|
23710
|
+
if (state.phase === "succeeded")
|
|
23711
|
+
engineRests.clear(authStatus[1], "signed-in");
|
|
23736
23712
|
return json(res, 200, { auth: state });
|
|
23737
23713
|
}
|
|
23738
23714
|
const instanceAction = /^\/api\/instances\/([\w.-]+)\/(refresh-models|install|auth\/start|auth\/complete|auth\/cancel|auth\/sign-out)$/.exec(path);
|
|
@@ -23774,6 +23750,8 @@ const handleRequest = async (req, res) => {
|
|
|
23774
23750
|
providerAuthSessions.revokeOwner(owner);
|
|
23775
23751
|
return json(res, 401, { error: "Your session ended. Start a new sign-in." });
|
|
23776
23752
|
}
|
|
23753
|
+
if (started.phase === "succeeded")
|
|
23754
|
+
engineRests.clear(instanceId, "signed-in");
|
|
23777
23755
|
return json(res, 200, { auth: started });
|
|
23778
23756
|
}
|
|
23779
23757
|
if (action === "auth/sign-out") {
|
|
@@ -23791,6 +23769,8 @@ const handleRequest = async (req, res) => {
|
|
|
23791
23769
|
if (!flowId || !callbackUrl)
|
|
23792
23770
|
return json(res, 400, { error: "flowId and a code or callbackUrl are required" });
|
|
23793
23771
|
await providerAuthSessions.complete(instanceId, owner, flowId, callbackUrl);
|
|
23772
|
+
engineRests.clear(instanceId, "signed-in");
|
|
23773
|
+
await describeEngines();
|
|
23794
23774
|
return json(res, 200, { ok: true });
|
|
23795
23775
|
}
|
|
23796
23776
|
const body = await readBody(req, 4096);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { GOAL_HELP } from "../shared/personal-goal.js";
|
|
2
|
+
import { PersonalGoals } from "./personal-goals.js";
|
|
3
|
+
/** Serial continuation per goal; every provider turn still passes normal admission. */
|
|
4
|
+
export class PersonalGoalRuntime {
|
|
5
|
+
goals;
|
|
6
|
+
running = new Set();
|
|
7
|
+
scheduled = new Map();
|
|
8
|
+
host;
|
|
9
|
+
constructor(path, host) {
|
|
10
|
+
this.host = host;
|
|
11
|
+
this.goals = new PersonalGoals(path, (goal) => host.changed(goal));
|
|
12
|
+
}
|
|
13
|
+
async command(botId, threadId, command, trigger) {
|
|
14
|
+
if (command.action === "help")
|
|
15
|
+
return GOAL_HELP;
|
|
16
|
+
if (this.goals.loadError)
|
|
17
|
+
return this.goals.loadError;
|
|
18
|
+
let goal = this.goals.get(threadId);
|
|
19
|
+
if (command.action === "status")
|
|
20
|
+
return goal ? this.describe(goal) : "This conversation has no goal. " + GOAL_HELP;
|
|
21
|
+
if (command.action === "start" || command.action === "resume") {
|
|
22
|
+
if (this.host.busy(botId, threadId) || (goal && this.running.has(goal.id)))
|
|
23
|
+
throw new Error("This conversation is still working. Wait for its turn to settle.");
|
|
24
|
+
goal = command.action === "start" ? this.goals.start(botId, threadId, { ...command, ...(trigger?.kind === "user" || trigger?.kind === "owner" ? { trigger } : {}) }) : this.goals.resume(threadId, command);
|
|
25
|
+
// Persist the human command before provider setup reads the conversation.
|
|
26
|
+
this.running.add(goal.id);
|
|
27
|
+
this.scheduled.set(goal.id, setImmediate(() => { this.scheduled.delete(goal.id); void this.run(goal); }));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
goal = this.goals.control(threadId, command.action);
|
|
31
|
+
const pending = this.scheduled.get(goal.id);
|
|
32
|
+
if (pending) {
|
|
33
|
+
clearImmediate(pending);
|
|
34
|
+
this.scheduled.delete(goal.id);
|
|
35
|
+
this.running.delete(goal.id);
|
|
36
|
+
}
|
|
37
|
+
if (this.running.has(goal.id))
|
|
38
|
+
await this.host.interrupt(botId, threadId, goal.id);
|
|
39
|
+
}
|
|
40
|
+
return this.describe(goal);
|
|
41
|
+
}
|
|
42
|
+
describe(goal) {
|
|
43
|
+
return `${goal.status}: ${goal.objective}\n${goal.detail}\nTurns: ${goal.turnCount}/${goal.maxTurns}. Tokens: ${goal.tokensUsed}${goal.tokenBudget === undefined ? "" : `/${goal.tokenBudget}`}.`;
|
|
44
|
+
}
|
|
45
|
+
steer(threadId) {
|
|
46
|
+
const goal = this.goals.get(threadId);
|
|
47
|
+
if (goal?.status === "working") {
|
|
48
|
+
goal.status = "paused";
|
|
49
|
+
goal.detail = "You sent a message. Use /goal resume when ready to continue.";
|
|
50
|
+
this.goals.touch(goal);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async run(goal) {
|
|
54
|
+
this.running.add(goal.id);
|
|
55
|
+
try {
|
|
56
|
+
while (this.goals.begin(goal.id)) {
|
|
57
|
+
const result = await this.host.run(goal);
|
|
58
|
+
this.goals.settle(goal.id, result);
|
|
59
|
+
// Give queued human input priority before reserving another turn.
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
if (goal.status === "working") {
|
|
65
|
+
goal.status = "paused";
|
|
66
|
+
goal.detail = "The next turn could not start. Check the conversation and use /goal resume.";
|
|
67
|
+
this.goals.touch(goal);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
this.running.delete(goal.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export async function receiveGoalCommand(runtime, store, botId, threadId, command, text, sendId, sender, trigger) {
|
|
76
|
+
let result;
|
|
77
|
+
try {
|
|
78
|
+
result = await runtime.command(botId, threadId, command, trigger);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw Object.assign(error instanceof Error ? error : new Error(String(error)), { status: 409 });
|
|
82
|
+
}
|
|
83
|
+
const message = store.appendMessage(threadId, { role: "user", kind: "text", text, sendId, sender });
|
|
84
|
+
store.appendMessage(threadId, { role: "bot", kind: "text", text: result });
|
|
85
|
+
return { ok: true, threadId, message, goalSummary: result };
|
|
86
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export function updatePersonalGoalReceipt(goal, store) {
|
|
2
|
+
if (!store.taskByThread(goal.botId, goal.threadId))
|
|
3
|
+
return;
|
|
4
|
+
const goalRun = { runId: goal.id, goal: goal.objective, status: goal.status,
|
|
5
|
+
coordinatorBotId: goal.botId, coordinatorName: store.bot(goal.botId)?.name ?? "Bot",
|
|
6
|
+
turnCount: goal.turnCount, maxTurns: goal.maxTurns, detail: `${goal.detail} Tokens: ${goal.tokensUsed}${goal.tokenBudget === undefined ? "" : `/${goal.tokenBudget}`}.`, startedAt: goal.createdAt };
|
|
7
|
+
if (goal.receiptMessageId)
|
|
8
|
+
store.patchMessage(goal.threadId, goal.receiptMessageId, { goalRun });
|
|
9
|
+
else
|
|
10
|
+
goal.receiptMessageId = store.appendMessage(goal.threadId, { role: "bot", kind: "goal.run", goalRun }).id;
|
|
11
|
+
}
|
|
12
|
+
export async function runPersonalGoalTurn(goal, host) {
|
|
13
|
+
const guard = { token: Symbol("personal-goal"), personalGoalId: goal.id, assistantItems: [], discard: false };
|
|
14
|
+
let ok = false, tools = 0, measured = false, completed = false;
|
|
15
|
+
let tokens;
|
|
16
|
+
const before = host.store.taskByThread(goal.botId, goal.threadId)?.usage;
|
|
17
|
+
const beforeTokens = (before?.input ?? 0) + (before?.output ?? 0);
|
|
18
|
+
host.addGuard(goal.threadId, guard);
|
|
19
|
+
const unsub = host.subscribe((event) => {
|
|
20
|
+
if (event.threadId !== goal.threadId || host.ignore(event))
|
|
21
|
+
return;
|
|
22
|
+
if (guard.turnId && event.turnId && guard.turnId !== event.turnId)
|
|
23
|
+
return;
|
|
24
|
+
if (event.type === "item.completed" && event.itemType === "tool")
|
|
25
|
+
tools++;
|
|
26
|
+
if (event.type === "thread.token-usage.updated")
|
|
27
|
+
measured = true;
|
|
28
|
+
if (event.type === "turn.completed") {
|
|
29
|
+
completed = true;
|
|
30
|
+
ok = event.ok;
|
|
31
|
+
measured ||= event.usage !== undefined;
|
|
32
|
+
const usage = host.store.taskByThread(goal.botId, goal.threadId)?.usage;
|
|
33
|
+
if (measured)
|
|
34
|
+
tokens = Math.max(0, (usage?.input ?? 0) + (usage?.output ?? 0) - beforeTokens);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
await new Promise((resolve, reject) => {
|
|
39
|
+
void host.start(goal.botId, goal.turnCount === 1 ? goal.objective : "Continue the active goal.", {
|
|
40
|
+
threadId: goal.threadId, cardContinuation: true, trigger: goal.trigger,
|
|
41
|
+
onTurnSettled: () => setImmediate(resolve), onDispatchError: reject,
|
|
42
|
+
}).catch(reject);
|
|
43
|
+
});
|
|
44
|
+
// The terminal event precedes digest/screen resource release. Never overlap those claims.
|
|
45
|
+
const deadline = Date.now() + 30_000;
|
|
46
|
+
while (host.busy(goal.botId, goal.threadId) && Date.now() < deadline)
|
|
47
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
48
|
+
if (host.busy(goal.botId, goal.threadId))
|
|
49
|
+
throw new Error("Turn is still settling");
|
|
50
|
+
return { ok, text: guard.assistantItems.join("\n"), tools, tokens };
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
unsub();
|
|
54
|
+
// Before a provider id exists, direct-turn admission owns late setup events.
|
|
55
|
+
// Keeping an unbound guard here would swallow an unrelated later turn.
|
|
56
|
+
if (completed || !guard.turnId)
|
|
57
|
+
host.removeGuard(goal.threadId, guard);
|
|
58
|
+
else {
|
|
59
|
+
guard.discard = true;
|
|
60
|
+
guard.cleanupTimer = setTimeout(() => host.removeGuard(goal.threadId, guard), 5 * 60_000);
|
|
61
|
+
guard.cleanupTimer.unref();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function addGoalGuard(groupGoalCoordinatorTurns, threadId, turn) {
|
|
66
|
+
const turns = groupGoalCoordinatorTurns.get(threadId) ?? new Set();
|
|
67
|
+
turns.add(turn);
|
|
68
|
+
groupGoalCoordinatorTurns.set(threadId, turns);
|
|
69
|
+
}
|
|
70
|
+
export function removeGoalGuard(groupGoalCoordinatorTurns, threadId, turn) {
|
|
71
|
+
if (turn.cleanupTimer)
|
|
72
|
+
clearTimeout(turn.cleanupTimer);
|
|
73
|
+
const turns = groupGoalCoordinatorTurns.get(threadId);
|
|
74
|
+
turns?.delete(turn);
|
|
75
|
+
if (turns?.size === 0)
|
|
76
|
+
groupGoalCoordinatorTurns.delete(threadId);
|
|
77
|
+
}
|
|
78
|
+
export function hasUnboundGoalGuard(groupGoalCoordinatorTurns, threadId) {
|
|
79
|
+
return [...(groupGoalCoordinatorTurns.get(threadId) ?? [])]
|
|
80
|
+
.some((turn) => turn.discard && !turn.turnId);
|
|
81
|
+
}
|
|
82
|
+
/** Match private coordinator output to one provider turn, never merely to a
|
|
83
|
+
* reusable room thread. Most adapters emit turn.started before sendTurn
|
|
84
|
+
* resolves, so the first stable event may bind an otherwise pending guard. */
|
|
85
|
+
export function goalGuardForEvent(groupGoalCoordinatorTurns, retireProviderTurn, event) {
|
|
86
|
+
const turns = groupGoalCoordinatorTurns.get(event.threadId);
|
|
87
|
+
if (!turns?.size)
|
|
88
|
+
return undefined;
|
|
89
|
+
const candidates = [...turns];
|
|
90
|
+
if (event.turnId) {
|
|
91
|
+
const exact = candidates.find((turn) => turn.turnId === event.turnId);
|
|
92
|
+
if (exact)
|
|
93
|
+
return exact;
|
|
94
|
+
// Until an interrupted handshake returns its own id, no new id can be
|
|
95
|
+
// attributed safely. The stall fallback keeps this thread unavailable in
|
|
96
|
+
// that narrow window; private text is suppressed below until sendTurn's
|
|
97
|
+
// result binds the old guard or its bounded expiry releases ownership.
|
|
98
|
+
const unboundDiscarded = candidates.filter((turn) => turn.discard && !turn.turnId);
|
|
99
|
+
if (unboundDiscarded.length > 0) {
|
|
100
|
+
// The ownership fallback below keeps a lone abandoned handshake's
|
|
101
|
+
// thread closed, so its first eventual id can safely bind here. More
|
|
102
|
+
// than one unbound candidate is genuinely ambiguous and stays gated.
|
|
103
|
+
if (candidates.length === 1) {
|
|
104
|
+
unboundDiscarded[0].turnId = event.turnId;
|
|
105
|
+
// This event is the first stable identity for an already-abandoned
|
|
106
|
+
// provider turn. Tombstone it immediately so this event and every
|
|
107
|
+
// later completion/request cannot settle a replacement on the same
|
|
108
|
+
// room thread.
|
|
109
|
+
retireProviderTurn(event.turnId);
|
|
110
|
+
return unboundDiscarded[0];
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const pending = candidates.findLast((turn) => !turn.turnId && !turn.discard);
|
|
115
|
+
if (pending && !pending.turnId) {
|
|
116
|
+
pending.turnId = event.turnId;
|
|
117
|
+
return pending;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
// Turn-scoped events normally carry an id. If an adapter omits it, fail
|
|
122
|
+
// closed for private text; with multiple overlapping guards there is no
|
|
123
|
+
// safe way to attribute a completion, so leave cleanup to the bounded timer.
|
|
124
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
125
|
+
}
|