@cotal-ai/connector-core 0.41.4 → 0.42.0
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/agent.d.ts +68 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +235 -2
- package/dist/agent.js.map +1 -1
- package/dist/docs-bundle.generated.js +7 -7
- package/dist/docs-bundle.generated.js.map +1 -1
- package/dist/tool-specs.d.ts.map +1 -1
- package/dist/tool-specs.js +24 -0
- package/dist/tool-specs.js.map +1 -1
- package/package.json +3 -3
package/dist/agent.d.ts
CHANGED
|
@@ -139,6 +139,16 @@ export declare class MeshAgent extends EventEmitter {
|
|
|
139
139
|
* to presence for peers. An absent key ⇒ that channel follows the global {@link _attention}. Reset
|
|
140
140
|
* on restart (rebuilt from config; presence sweep clears the mirror). */
|
|
141
141
|
private channelModes;
|
|
142
|
+
/** The turn relay's seat-side intake: every pulled-and-unyielded run turn, by goal id. Fed by
|
|
143
|
+
* {@link pollTurns}; surfaced into host context by {@link surfacePendingTurns}; drained by
|
|
144
|
+
* {@link yieldTurn} (explicit) and the working→idle boundary in {@link setStatus} (automatic
|
|
145
|
+
* `done`). The poll is also the reconciler: a turn settled elsewhere (deadline, another yield
|
|
146
|
+
* path) vanishes from `turn-pending` and is dropped here on the next pull. */
|
|
147
|
+
private activeTurns;
|
|
148
|
+
private turnPollTimer?;
|
|
149
|
+
/** The last pull failure this seat reported, so one that keeps failing is said once. */
|
|
150
|
+
private pullTrouble?;
|
|
151
|
+
private turnPollBusy;
|
|
142
152
|
private _contextId;
|
|
143
153
|
/** Chat-stream frontier captured when this agent entered `focus` — recall surfaces ambient
|
|
144
154
|
* published after it ("since you entered focus"). Undefined unless in focus. */
|
|
@@ -466,6 +476,47 @@ export declare class MeshAgent extends EventEmitter {
|
|
|
466
476
|
despawn(name?: string, opts?: {
|
|
467
477
|
graceful?: boolean;
|
|
468
478
|
}): Promise<ControlReply>;
|
|
479
|
+
private ensureTurnPoll;
|
|
480
|
+
/** Pull this seat's pending run turns from the manager (`turn-pending`, self-mode). New turns
|
|
481
|
+
* request a wake so the payload surfaces on the next injectable frame; turns gone from the
|
|
482
|
+
* reply were settled elsewhere (deadline, agent-down, a competing yield path) and are dropped.
|
|
483
|
+
* A seat with no manager in its space, or no self-service reach, gets a refused invoke and
|
|
484
|
+
* simply has no relay — silence, not an error, because most joined sessions are exactly that. */
|
|
485
|
+
private pollTurns;
|
|
486
|
+
/**
|
|
487
|
+
* A pull that did not come back, said ONCE.
|
|
488
|
+
*
|
|
489
|
+
* The poll runs every few seconds for the life of the session, so a line per failure would be a
|
|
490
|
+
* torrent and there was none at all instead: a seat whose relay had gone quiet looked exactly
|
|
491
|
+
* like a seat with no relay, and neither the operator nor the agent could tell which. The
|
|
492
|
+
* ordinary shapes stay silent, because most joined sessions genuinely have no manager to pull
|
|
493
|
+
* from and that is not trouble. Everything else is said once per distinct reason, and saying it
|
|
494
|
+
* again waits for the reason to change or for a pull to succeed.
|
|
495
|
+
*/
|
|
496
|
+
private notePullTrouble;
|
|
497
|
+
/** Format every not-yet-surfaced turn as an injectable context block, WITHOUT marking anything
|
|
498
|
+
* (undefined when none wait). Two-phase on purpose, like the inbox's format-then-verdict rail:
|
|
499
|
+
* marking at format time would let a lost frame auto-yield `done` for work the model never
|
|
500
|
+
* saw. A caller commits with {@link commitSurfacedTurns} only once the injection verifiably
|
|
501
|
+
* reached the host. The payload is opaque relay bytes; when it parses as JSON with a string
|
|
502
|
+
* `context`, that rendered context is what the model reads, else the raw payload is shown. */
|
|
503
|
+
peekPendingTurns(): {
|
|
504
|
+
text: string;
|
|
505
|
+
goalIds: string[];
|
|
506
|
+
} | undefined;
|
|
507
|
+
/** Mark peeked turns as surfaced — the second phase, called once their injection verifiably
|
|
508
|
+
* reached the host. Surfacing is what arms the automatic `done` at the next turn boundary; a
|
|
509
|
+
* turn never committed re-surfaces on a later frame instead of yielding a lie. */
|
|
510
|
+
commitSurfacedTurns(goalIds: readonly string[]): void;
|
|
511
|
+
/** Yield one active turn back to the run (`turn-yield`, self-mode). Without `turn` it targets
|
|
512
|
+
* the OLDEST surfaced turn — the one the current session turn is working on. The entry is
|
|
513
|
+
* dropped locally only on a confirmed yield; a refused one stays for the poll to reconcile
|
|
514
|
+
* (the manager's answer, not a local guess, decides whether it is settled). */
|
|
515
|
+
yieldTurn(status: "done" | "blocked" | "handoff", opts?: {
|
|
516
|
+
to?: string;
|
|
517
|
+
note?: string;
|
|
518
|
+
turn?: string;
|
|
519
|
+
}): Promise<ControlReply>;
|
|
469
520
|
/** Ask the manager to purge the space's retained chat backlog (its `purge` op). Cleanup only —
|
|
470
521
|
* it doesn't touch live agents or the anycast work queue. `includeDms` also clears DM history. */
|
|
471
522
|
purgeHistory(opts?: {
|
|
@@ -510,6 +561,23 @@ export declare class MeshAgent extends EventEmitter {
|
|
|
510
561
|
/** Our last self-reported presence status. */
|
|
511
562
|
get status(): PresenceStatus;
|
|
512
563
|
setStatus(status: PresenceStatus, activity?: string): Promise<void>;
|
|
564
|
+
/**
|
|
565
|
+
* Publish presence for something that is NOT a turn ending.
|
|
566
|
+
*
|
|
567
|
+
* The boundary above reads working→idle as "the seat finished its turn", and that reading holds
|
|
568
|
+
* only at a real turn terminal. A session (re)start writes idle too: Claude Code fires
|
|
569
|
+
* `SessionStart` on compact, clear and resume, and an auto-compaction lands in the MIDDLE of a
|
|
570
|
+
* long turn. Routed through `setStatus` it yielded `done` for work the model had not finished,
|
|
571
|
+
* and the run moved on. An adapter uses this for every idle that is a lifecycle event rather
|
|
572
|
+
* than an ending.
|
|
573
|
+
*/
|
|
574
|
+
resetStatus(status: PresenceStatus, activity?: string): Promise<void>;
|
|
575
|
+
private publishStatus;
|
|
576
|
+
/** The working→idle boundary: yield `done` for every SURFACED turn (its payload was in the
|
|
577
|
+
* context of the turn that just ended; ending without an explicit yield IS the done signal),
|
|
578
|
+
* then re-poll immediately so a queued turn wakes the seat without waiting out the cadence.
|
|
579
|
+
* Fire-and-forget from the presence path — a yield must never block a status write. */
|
|
580
|
+
private onTurnBoundary;
|
|
513
581
|
/** Record the host's actual model and optional variant learned after launch, so peers see the
|
|
514
582
|
* selection in `cotal_roster` and the web roster even when the operator never pinned one. Explicit
|
|
515
583
|
* `model:` / `variant:` config wins; this only fills the gap. Best-effort presence mirror (no
|
package/dist/agent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAOL,aAAa,EAQb,KAAK,YAAY,EAIjB,KAAK,QAAQ,EACb,KAAK,cAAc,EAEnB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,CAAC;AAE3C;;;6DAG6D;AAC7D,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC;;;;;;8EAM8E;AAC9E,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC;;8BAE8B;AAC9B,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAItD;AAgCD,2EAA2E;AAC3E,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX;;;;;;6EAMyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,CAAC;IACnC,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,kGAAkG;IAClG,UAAU,EAAE,OAAO,CAAC;IACpB,+FAA+F;IAC/F,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAaD,0GAA0G;AAC1G,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,0GAA0G;AAC1G,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CASrE;
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
|
package/dist/agent.js
CHANGED
|
@@ -83,6 +83,11 @@ const PROTECTED_DISPOSITION_CAP = 4096;
|
|
|
83
83
|
/** Repeated async NATS status errors can arrive once per ordered-consumer retry. They describe one
|
|
84
84
|
* fault, not one hundred useful facts; keep the first visible and summarize at most twice a minute. */
|
|
85
85
|
const ENDPOINT_ERROR_LOG_WINDOW_MS = 30_000;
|
|
86
|
+
/** Cadence of the turn-pending pull while connected. The relay is PULL-shaped by design (the
|
|
87
|
+
* manager holds no DM machinery and a pushed relay could not authenticate its sender), so this
|
|
88
|
+
* poll is the intake's whole latency: a fresh turn waits at most one interval plus one wake.
|
|
89
|
+
* Deadlines are minutes-scale; fifteen seconds of intake lag is invisible to a run. */
|
|
90
|
+
const TURN_POLL_MS = 15_000;
|
|
86
91
|
import { randomUUID } from "node:crypto";
|
|
87
92
|
function sleep(ms) {
|
|
88
93
|
return new Promise((r) => setTimeout(r, ms));
|
|
@@ -95,6 +100,40 @@ function sleep(ms) {
|
|
|
95
100
|
function ingestDedupKey(id) {
|
|
96
101
|
return id === "" ? undefined : id;
|
|
97
102
|
}
|
|
103
|
+
/** An `ask` relayed as a turn: the record the run needs, the attempt it is on, the previous
|
|
104
|
+
* refusal when there was one, and the command that answers it (`cotal run answer`, the same door
|
|
105
|
+
* a checkpoint is answered through). Empty when the payload carries no ask. */
|
|
106
|
+
function renderAskRequest(p, seatName) {
|
|
107
|
+
const escalation = renderEscalation(p, seatName);
|
|
108
|
+
if (escalation !== "")
|
|
109
|
+
return escalation;
|
|
110
|
+
const ask = p.ask;
|
|
111
|
+
if (ask === null || typeof ask !== "object")
|
|
112
|
+
return "";
|
|
113
|
+
const a = ask;
|
|
114
|
+
const entries = a.schema !== null && typeof a.schema === "object" ? Object.entries(a.schema) : [];
|
|
115
|
+
const fields = entries.length === 0 ? "any record" : entries.map(([k, v]) => `${k}: ${String(v)}`).join(", ");
|
|
116
|
+
const by = typeof a.deadlineAt === "number" ? `, by ${new Date(a.deadlineAt).toISOString()}` : "";
|
|
117
|
+
const refused = typeof a.refused === "string" ? `\nYour last answer was refused: ${a.refused}` : "";
|
|
118
|
+
return `\nThis turn is an ask: the run needs a record from you at step ${String(p.step)} with fields ${fields}`
|
|
119
|
+
+ ` (attempt ${String(a.attempt)} of ${String(a.attempts)}${by}).${refused}`
|
|
120
|
+
+ `\nAnswer with: cotal run answer ${String(p.run)} ${String(p.step)} --by ${seatName} --value '<json record>'`;
|
|
121
|
+
}
|
|
122
|
+
/** An escalated `checkpoint` relayed as a turn: the question, the record wanted when the pause
|
|
123
|
+
* carries a schema, and the same answer command an ask uses. The runtime submitted this shape from
|
|
124
|
+
* the day escalations were relayed, and the seat read `ask` alone: the addressee was woken with a
|
|
125
|
+
* context block and no question, auto-yielded `done`, and the pause ran to its own expiry. */
|
|
126
|
+
function renderEscalation(p, seatName) {
|
|
127
|
+
const cp = p.checkpoint;
|
|
128
|
+
if (cp === null || typeof cp !== "object")
|
|
129
|
+
return "";
|
|
130
|
+
const c = cp;
|
|
131
|
+
const entries = c.schema !== null && typeof c.schema === "object" ? Object.entries(c.schema) : [];
|
|
132
|
+
const wanted = entries.length === 0 ? "" : `\nAnswer with a record with fields ${entries.map(([k, v]) => `${k}: ${String(v)}`).join(", ")}.`;
|
|
133
|
+
const by = typeof c.deadlineAt === "number" ? ` It expires at ${new Date(c.deadlineAt).toISOString()}.` : "";
|
|
134
|
+
return `\nThis turn is a checkpoint escalated to you at step ${String(p.step)}: ${String(c.prompt)}${by}${wanted}`
|
|
135
|
+
+ `\nAnswer with: cotal run answer ${String(p.run)} ${String(p.step)} --by ${seatName} --value '<json>'`;
|
|
136
|
+
}
|
|
98
137
|
export class MeshAgent extends EventEmitter {
|
|
99
138
|
ep;
|
|
100
139
|
config;
|
|
@@ -148,6 +187,16 @@ export class MeshAgent extends EventEmitter {
|
|
|
148
187
|
* to presence for peers. An absent key ⇒ that channel follows the global {@link _attention}. Reset
|
|
149
188
|
* on restart (rebuilt from config; presence sweep clears the mirror). */
|
|
150
189
|
channelModes = new Map();
|
|
190
|
+
/** The turn relay's seat-side intake: every pulled-and-unyielded run turn, by goal id. Fed by
|
|
191
|
+
* {@link pollTurns}; surfaced into host context by {@link surfacePendingTurns}; drained by
|
|
192
|
+
* {@link yieldTurn} (explicit) and the working→idle boundary in {@link setStatus} (automatic
|
|
193
|
+
* `done`). The poll is also the reconciler: a turn settled elsewhere (deadline, another yield
|
|
194
|
+
* path) vanishes from `turn-pending` and is dropped here on the next pull. */
|
|
195
|
+
activeTurns = new Map();
|
|
196
|
+
turnPollTimer;
|
|
197
|
+
/** The last pull failure this seat reported, so one that keeps failing is said once. */
|
|
198
|
+
pullTrouble;
|
|
199
|
+
turnPollBusy = false;
|
|
151
200
|
_contextId;
|
|
152
201
|
/** Chat-stream frontier captured when this agent entered `focus` — recall surfaces ambient
|
|
153
202
|
* published after it ("since you entered focus"). Undefined unless in focus. */
|
|
@@ -323,6 +372,7 @@ export class MeshAgent extends EventEmitter {
|
|
|
323
372
|
await this.ep.start();
|
|
324
373
|
// _connected is set by the endpoint's "connection" event (fired inside start()), not here.
|
|
325
374
|
this.log(`connected to ${this.config.servers} as ${this.who()} in space "${this.config.space}" on #${this.config.subscribe.join(", #")}`);
|
|
375
|
+
this.ensureTurnPoll();
|
|
326
376
|
}
|
|
327
377
|
catch (e) {
|
|
328
378
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
@@ -339,6 +389,10 @@ export class MeshAgent extends EventEmitter {
|
|
|
339
389
|
}
|
|
340
390
|
async stop() {
|
|
341
391
|
this._stopping = true;
|
|
392
|
+
if (this.turnPollTimer !== undefined) {
|
|
393
|
+
clearInterval(this.turnPollTimer);
|
|
394
|
+
this.turnPollTimer = undefined;
|
|
395
|
+
}
|
|
342
396
|
// stop() is a local terminal fact. Do not wait for an endpoint event that intentionally ignores
|
|
343
397
|
// its own stopped close, or leave a cleanly stopped session reporting either state as live.
|
|
344
398
|
this._connected = false;
|
|
@@ -887,7 +941,10 @@ export class MeshAgent extends EventEmitter {
|
|
|
887
941
|
* Subsumes {@link directedPendingCount}: in `dnd`/`focus` (no override) the open term is false, so it
|
|
888
942
|
* equals the directed count; in `open` it adds normal ambient but excludes quiet-channel ambient. */
|
|
889
943
|
pendingWake() {
|
|
890
|
-
|
|
944
|
+
// An unsurfaced run turn is directed-strength: it wakes regardless of attention, exactly like
|
|
945
|
+
// a DM — a run is waiting on this seat, and holding it costs the run its deadline.
|
|
946
|
+
const turns = [...this.activeTurns.values()].filter((t) => !t.surfaced).length;
|
|
947
|
+
return turns + this.inbox.filter((p) => {
|
|
891
948
|
const it = p.item;
|
|
892
949
|
if (p.pullOnly)
|
|
893
950
|
return false;
|
|
@@ -1221,6 +1278,131 @@ export class MeshAgent extends EventEmitter {
|
|
|
1221
1278
|
return resolved.error;
|
|
1222
1279
|
return this.managerInvoke("despawn", { graceful }, { target: resolved.target });
|
|
1223
1280
|
}
|
|
1281
|
+
// ---- the turn relay (seat side) ------------------------------------------------------------
|
|
1282
|
+
ensureTurnPoll() {
|
|
1283
|
+
if (this.turnPollTimer !== undefined)
|
|
1284
|
+
return;
|
|
1285
|
+
const t = setInterval(() => {
|
|
1286
|
+
void this.pollTurns().catch((e) => this.notePullTrouble(e.message));
|
|
1287
|
+
}, TURN_POLL_MS);
|
|
1288
|
+
t.unref?.();
|
|
1289
|
+
this.turnPollTimer = t;
|
|
1290
|
+
}
|
|
1291
|
+
/** Pull this seat's pending run turns from the manager (`turn-pending`, self-mode). New turns
|
|
1292
|
+
* request a wake so the payload surfaces on the next injectable frame; turns gone from the
|
|
1293
|
+
* reply were settled elsewhere (deadline, agent-down, a competing yield path) and are dropped.
|
|
1294
|
+
* A seat with no manager in its space, or no self-service reach, gets a refused invoke and
|
|
1295
|
+
* simply has no relay — silence, not an error, because most joined sessions are exactly that. */
|
|
1296
|
+
async pollTurns() {
|
|
1297
|
+
if (!this._connected || this._stopping || this.turnPollBusy)
|
|
1298
|
+
return;
|
|
1299
|
+
this.turnPollBusy = true;
|
|
1300
|
+
try {
|
|
1301
|
+
const r = await this.managerInvoke("turn-pending", undefined, { target: { mode: "self" } });
|
|
1302
|
+
if (!r.ok) {
|
|
1303
|
+
this.notePullTrouble(r.error ?? "refused with no message");
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
this.pullTrouble = undefined;
|
|
1307
|
+
const turns = r.data?.turns ?? [];
|
|
1308
|
+
const live = new Set(turns.map((t) => t.goalId));
|
|
1309
|
+
for (const id of [...this.activeTurns.keys()])
|
|
1310
|
+
if (!live.has(id))
|
|
1311
|
+
this.activeTurns.delete(id);
|
|
1312
|
+
let fresh = 0;
|
|
1313
|
+
for (const t of turns) {
|
|
1314
|
+
if (this.activeTurns.has(t.goalId))
|
|
1315
|
+
continue;
|
|
1316
|
+
this.activeTurns.set(t.goalId, { goalId: t.goalId, payload: t.payload, acceptedAt: t.acceptedAt, deadlineAt: t.deadlineAt, surfaced: false });
|
|
1317
|
+
fresh += 1;
|
|
1318
|
+
}
|
|
1319
|
+
if (fresh > 0)
|
|
1320
|
+
this.requestWake();
|
|
1321
|
+
}
|
|
1322
|
+
finally {
|
|
1323
|
+
this.turnPollBusy = false;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* A pull that did not come back, said ONCE.
|
|
1328
|
+
*
|
|
1329
|
+
* The poll runs every few seconds for the life of the session, so a line per failure would be a
|
|
1330
|
+
* torrent and there was none at all instead: a seat whose relay had gone quiet looked exactly
|
|
1331
|
+
* like a seat with no relay, and neither the operator nor the agent could tell which. The
|
|
1332
|
+
* ordinary shapes stay silent, because most joined sessions genuinely have no manager to pull
|
|
1333
|
+
* from and that is not trouble. Everything else is said once per distinct reason, and saying it
|
|
1334
|
+
* again waits for the reason to change or for a pull to succeed.
|
|
1335
|
+
*/
|
|
1336
|
+
notePullTrouble(reason) {
|
|
1337
|
+
// "Nobody answered" is the no-relay case this poll expects, and the boot window is a manager
|
|
1338
|
+
// telling the seat to come back — both resolve themselves and neither is worth a line.
|
|
1339
|
+
if (/no responder answered|still reconciling/i.test(reason))
|
|
1340
|
+
return;
|
|
1341
|
+
if (this.pullTrouble === reason)
|
|
1342
|
+
return;
|
|
1343
|
+
this.pullTrouble = reason;
|
|
1344
|
+
this.log(`the run-turn pull is not answering (${reason}); retrying every ${TURN_POLL_MS}ms`);
|
|
1345
|
+
}
|
|
1346
|
+
/** Format every not-yet-surfaced turn as an injectable context block, WITHOUT marking anything
|
|
1347
|
+
* (undefined when none wait). Two-phase on purpose, like the inbox's format-then-verdict rail:
|
|
1348
|
+
* marking at format time would let a lost frame auto-yield `done` for work the model never
|
|
1349
|
+
* saw. A caller commits with {@link commitSurfacedTurns} only once the injection verifiably
|
|
1350
|
+
* reached the host. The payload is opaque relay bytes; when it parses as JSON with a string
|
|
1351
|
+
* `context`, that rendered context is what the model reads, else the raw payload is shown. */
|
|
1352
|
+
peekPendingTurns() {
|
|
1353
|
+
const waiting = [...this.activeTurns.values()].filter((t) => !t.surfaced).sort((a, b) => a.acceptedAt - b.acceptedAt);
|
|
1354
|
+
if (!waiting.length)
|
|
1355
|
+
return undefined;
|
|
1356
|
+
const blocks = waiting.map((t) => {
|
|
1357
|
+
let context = t.payload;
|
|
1358
|
+
let ask = "";
|
|
1359
|
+
try {
|
|
1360
|
+
const p = JSON.parse(t.payload);
|
|
1361
|
+
if (typeof p.context === "string" && p.context.length > 0)
|
|
1362
|
+
context = p.context;
|
|
1363
|
+
ask = renderAskRequest(p, this.config.name);
|
|
1364
|
+
}
|
|
1365
|
+
catch { /* opaque payload — surface it as it came */ }
|
|
1366
|
+
return `— turn ${t.goalId} (deadline ${new Date(t.deadlineAt).toISOString()}):\n${context}${ask}`;
|
|
1367
|
+
});
|
|
1368
|
+
const text = `🎯 Cotal — a run handed you ${waiting.length === 1 ? "its turn" : `${waiting.length} turns`}:\n` +
|
|
1369
|
+
`${blocks.join("\n")}\n` +
|
|
1370
|
+
`(Do the work now with your own tools. Ending your turn yields "done" back to the run automatically; ` +
|
|
1371
|
+
`call cotal_yield only when you are blocked or handing the turn to another agent.)`;
|
|
1372
|
+
return { text, goalIds: waiting.map((t) => t.goalId) };
|
|
1373
|
+
}
|
|
1374
|
+
/** Mark peeked turns as surfaced — the second phase, called once their injection verifiably
|
|
1375
|
+
* reached the host. Surfacing is what arms the automatic `done` at the next turn boundary; a
|
|
1376
|
+
* turn never committed re-surfaces on a later frame instead of yielding a lie. */
|
|
1377
|
+
commitSurfacedTurns(goalIds) {
|
|
1378
|
+
for (const id of goalIds) {
|
|
1379
|
+
const t = this.activeTurns.get(id);
|
|
1380
|
+
if (t)
|
|
1381
|
+
t.surfaced = true;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
/** Yield one active turn back to the run (`turn-yield`, self-mode). Without `turn` it targets
|
|
1385
|
+
* the OLDEST surfaced turn — the one the current session turn is working on. The entry is
|
|
1386
|
+
* dropped locally only on a confirmed yield; a refused one stays for the poll to reconcile
|
|
1387
|
+
* (the manager's answer, not a local guess, decides whether it is settled). */
|
|
1388
|
+
async yieldTurn(status, opts = {}) {
|
|
1389
|
+
await this.requireConnected();
|
|
1390
|
+
const t = opts.turn !== undefined
|
|
1391
|
+
? this.activeTurns.get(opts.turn)
|
|
1392
|
+
: [...this.activeTurns.values()].filter((x) => x.surfaced).sort((a, b) => a.acceptedAt - b.acceptedAt)[0];
|
|
1393
|
+
if (!t)
|
|
1394
|
+
return { ok: false, error: opts.turn !== undefined ? `no active turn "${opts.turn}" on this seat` : "no turn is active — nothing to yield" };
|
|
1395
|
+
// The surfaced gate holds for an explicit id too: a yield for a payload the session has not
|
|
1396
|
+
// been shown would record work nobody did.
|
|
1397
|
+
if (!t.surfaced)
|
|
1398
|
+
return { ok: false, error: `turn "${t.goalId}" has not been surfaced into this session yet; nothing was seen, so nothing can be yielded for it` };
|
|
1399
|
+
if (status === "handoff" && (opts.to === undefined || opts.to.length === 0))
|
|
1400
|
+
return { ok: false, error: "a handoff yield names its addressee (to)" };
|
|
1401
|
+
const r = await this.managerInvoke("turn-yield", { goalId: t.goalId, status, to: opts.to, note: opts.note }, { target: { mode: "self" } });
|
|
1402
|
+
if (r.ok)
|
|
1403
|
+
this.activeTurns.delete(t.goalId);
|
|
1404
|
+
return r;
|
|
1405
|
+
}
|
|
1224
1406
|
/** Ask the manager to purge the space's retained chat backlog (its `purge` op). Cleanup only —
|
|
1225
1407
|
* it doesn't touch live agents or the anycast work queue. `includeDms` also clears DM history. */
|
|
1226
1408
|
async purgeHistory(opts) {
|
|
@@ -1331,11 +1513,62 @@ export class MeshAgent extends EventEmitter {
|
|
|
1331
1513
|
}
|
|
1332
1514
|
async setStatus(status, activity) {
|
|
1333
1515
|
await this.requireConnected();
|
|
1334
|
-
this._status
|
|
1516
|
+
const prev = this._status;
|
|
1517
|
+
try {
|
|
1518
|
+
await this.publishStatus(status, activity);
|
|
1519
|
+
}
|
|
1520
|
+
finally {
|
|
1521
|
+
// The transition is a fact about the SEAT, not about whether its presence row was written:
|
|
1522
|
+
// assigning before the writes meant one failed publish (which every adapter swallows) left
|
|
1523
|
+
// `_status` idle with no boundary run, and the next real turn end saw no transition at all.
|
|
1524
|
+
this._status = status;
|
|
1525
|
+
// The turn relay's boundary: an adapter funnels its turn-end through this transition (Stop
|
|
1526
|
+
// hooks set idle), so the automatic `done` yield and the immediate re-poll live here once
|
|
1527
|
+
// instead of per connector. `waiting` is not a boundary — a seat blocked on a permission has
|
|
1528
|
+
// not finished, and its deadline is what bounds a stuck one.
|
|
1529
|
+
if (prev === "working" && status === "idle")
|
|
1530
|
+
this.onTurnBoundary();
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Publish presence for something that is NOT a turn ending.
|
|
1535
|
+
*
|
|
1536
|
+
* The boundary above reads working→idle as "the seat finished its turn", and that reading holds
|
|
1537
|
+
* only at a real turn terminal. A session (re)start writes idle too: Claude Code fires
|
|
1538
|
+
* `SessionStart` on compact, clear and resume, and an auto-compaction lands in the MIDDLE of a
|
|
1539
|
+
* long turn. Routed through `setStatus` it yielded `done` for work the model had not finished,
|
|
1540
|
+
* and the run moved on. An adapter uses this for every idle that is a lifecycle event rather
|
|
1541
|
+
* than an ending.
|
|
1542
|
+
*/
|
|
1543
|
+
async resetStatus(status, activity) {
|
|
1544
|
+
await this.requireConnected();
|
|
1545
|
+
try {
|
|
1546
|
+
await this.publishStatus(status, activity);
|
|
1547
|
+
}
|
|
1548
|
+
finally {
|
|
1549
|
+
this._status = status;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
async publishStatus(status, activity) {
|
|
1335
1553
|
if (activity !== undefined)
|
|
1336
1554
|
await this.ep.setActivity(activity);
|
|
1337
1555
|
await this.ep.setStatus(status);
|
|
1338
1556
|
}
|
|
1557
|
+
/** The working→idle boundary: yield `done` for every SURFACED turn (its payload was in the
|
|
1558
|
+
* context of the turn that just ended; ending without an explicit yield IS the done signal),
|
|
1559
|
+
* then re-poll immediately so a queued turn wakes the seat without waiting out the cadence.
|
|
1560
|
+
* Fire-and-forget from the presence path — a yield must never block a status write. */
|
|
1561
|
+
onTurnBoundary() {
|
|
1562
|
+
for (const t of [...this.activeTurns.values()]) {
|
|
1563
|
+
if (!t.surfaced)
|
|
1564
|
+
continue;
|
|
1565
|
+
void this.yieldTurn("done", { turn: t.goalId })
|
|
1566
|
+
.then((r) => { if (!r.ok)
|
|
1567
|
+
this.log(`turn ${t.goalId} auto-yield: ${r.error ?? "refused"}`); })
|
|
1568
|
+
.catch((e) => this.log(`turn ${t.goalId} auto-yield: ${e.message}`));
|
|
1569
|
+
}
|
|
1570
|
+
void this.pollTurns().catch(() => { });
|
|
1571
|
+
}
|
|
1339
1572
|
/** Record the host's actual model and optional variant learned after launch, so peers see the
|
|
1340
1573
|
* selection in `cotal_roster` and the web roster even when the operator never pinned one. Explicit
|
|
1341
1574
|
* `model:` / `variant:` config wins; this only fills the gap. Best-effort presence mirror (no
|