@songsid/agend 2.1.6 → 2.1.7-beta.2
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/backend/codex.d.ts +8 -3
- package/dist/backend/codex.js +118 -9
- package/dist/backend/codex.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +1 -0
- package/dist/channel/adapters/discord.js +25 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +13 -0
- package/dist/channel/adapters/telegram.js +44 -3
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +4 -0
- package/dist/daemon.d.ts +6 -1
- package/dist/daemon.js +70 -27
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +23 -8
- package/dist/fleet-manager.js +101 -28
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -1
- package/dist/instance-lifecycle.js +53 -9
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +5 -1
- package/dist/turn-reply-guard.js +6 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -37,11 +37,17 @@ const CROSS_INSTANCE_TOOLS = new Set(["send_to_instance", "list_instances", "sta
|
|
|
37
37
|
const SCHEDULE_TOOLS = new Set(["create_schedule", "list_schedules", "update_schedule", "delete_schedule"]);
|
|
38
38
|
const DECISION_TOOLS = new Set(["post_decision", "list_decisions", "update_decision"]);
|
|
39
39
|
const TASK_TOOL = "task";
|
|
40
|
-
// Tools whose success proves the agent
|
|
41
|
-
// of these succeeded, a dead-MCP proxy reply would
|
|
42
|
-
const
|
|
40
|
+
// Tools whose success proves the agent performed an outbound action this turn.
|
|
41
|
+
// While any of these succeeded, a dead-MCP proxy reply would duplicate output.
|
|
42
|
+
const TURN_OUTBOUND_TOOLS = new Set([
|
|
43
|
+
"reply", "react", "edit_message", "send_to_instance", "report_result",
|
|
44
|
+
"request_information", "delegate_task", "broadcast",
|
|
45
|
+
]);
|
|
46
|
+
// These actions reach the human-facing channel and satisfy reply-completion:
|
|
47
|
+
// a deliberate reaction or edit is a valid response even without new text.
|
|
48
|
+
const TURN_COMPLETION_TOOLS = new Set(["reply", "react", "edit_message"]);
|
|
43
49
|
const REPLY_DROP_WARNING_COOLDOWN_MS = 5 * 60_000;
|
|
44
|
-
const REPLY_RECOVERY_PROMPT = "[system:reply-required] The previous human-facing turn ended without a successfully delivered reply. Do not redo the work.
|
|
50
|
+
const REPLY_RECOVERY_PROMPT = "[system:reply-required] The previous human-facing turn ended without a successfully delivered reply. Do not redo the work. React with an emoji or use the reply tool exactly once now to send the user a concise conclusion. If no substantive answer is needed, a brief react is sufficient. Do not reply to this system instruction except through the react or reply tool.";
|
|
45
51
|
/** Point a resumed CLI at its one backend-native instruction source. */
|
|
46
52
|
export function buildInstructionReloadNotice(binaryName, instanceName, instanceDir) {
|
|
47
53
|
const source = binaryName === "codex" || binaryName === "grok" || binaryName === "muse"
|
|
@@ -495,6 +501,15 @@ const INPUT_TRANSIENT_WAIT_MS = 10 * 60_000;
|
|
|
495
501
|
const INPUT_TRANSIENT_POLL_MS = 250;
|
|
496
502
|
/** Max "stranded text → submit → wait for prompt → re-check" rounds per delivery; each may send one recovery Enter. */
|
|
497
503
|
const STRANDED_INPUT_MAX_ROUNDS = 3;
|
|
504
|
+
/** Build a status holder from channel metadata, keeping chat and thread apart. */
|
|
505
|
+
function channelStatus(meta) {
|
|
506
|
+
const chatId = meta.chat_id;
|
|
507
|
+
const messageId = meta.message_id;
|
|
508
|
+
if (!chatId || !messageId)
|
|
509
|
+
return undefined;
|
|
510
|
+
const threadId = meta.thread_id || undefined;
|
|
511
|
+
return threadId ? { chatId, messageId, threadId } : { chatId, messageId };
|
|
512
|
+
}
|
|
498
513
|
const FIRST_ENTER_SETTLE_MS = 1_750;
|
|
499
514
|
const FIRST_DELIVERY_WINDOW_MS = 5_000;
|
|
500
515
|
/** After busy native-queue paste+Enter, wait before checking the pane for silent loss. */
|
|
@@ -1214,10 +1229,26 @@ export class Daemon extends EventEmitter {
|
|
|
1214
1229
|
// capture buffer the count drops — we lower the baseline so a re-occurrence
|
|
1215
1230
|
// still registers as new (prevents the old hash-dedup's permanent suppression).
|
|
1216
1231
|
lastErrorCount = new Map();
|
|
1232
|
+
// A lifecycle-provided count is a lower bound while a restarted Codex pane is
|
|
1233
|
+
// still booting. Its new tmux window starts empty before resume restores the
|
|
1234
|
+
// old transcript, so pre-ready scans must not lower this seeded baseline.
|
|
1235
|
+
seededErrorCountBaselines = new Map();
|
|
1217
1236
|
lastDetectedErrorType = null;
|
|
1218
1237
|
static errorPatternKey(ep) {
|
|
1219
1238
|
return `${ep.type}:${ep.pattern.source}`;
|
|
1220
1239
|
}
|
|
1240
|
+
/** Read the occurrence baseline so a lifecycle-owned restart can carry it forward. */
|
|
1241
|
+
getErrorPatternOccurrenceCount(type, pattern) {
|
|
1242
|
+
return this.lastErrorCount.get(`${type}:${pattern.source}`) ?? 0;
|
|
1243
|
+
}
|
|
1244
|
+
/** Seed a replacement daemon with an already-reported pattern count. */
|
|
1245
|
+
seedErrorPatternOccurrenceCount(type, pattern, count) {
|
|
1246
|
+
if (!Number.isSafeInteger(count) || count <= 0)
|
|
1247
|
+
return;
|
|
1248
|
+
const key = `${type}:${pattern.source}`;
|
|
1249
|
+
this.lastErrorCount.set(key, count);
|
|
1250
|
+
this.seededErrorCountBaselines.set(key, count);
|
|
1251
|
+
}
|
|
1221
1252
|
/**
|
|
1222
1253
|
* Undo the auth suspicion a pattern match armed, once the backend's token-free
|
|
1223
1254
|
* probe has said the credentials are fine.
|
|
@@ -2440,6 +2471,23 @@ export class Daemon extends EventEmitter {
|
|
|
2440
2471
|
// notification, a false recovery log, then silence.
|
|
2441
2472
|
const looksReady = () => !busyPattern?.test(pane) && readyPattern.test(pane);
|
|
2442
2473
|
const codexLivePane = this.isCodexLivePaneSnapshot(pane);
|
|
2474
|
+
const rebasedSeededBaselines = new Set();
|
|
2475
|
+
// The baseline belongs to the resumed transcript, not the empty startup
|
|
2476
|
+
// frame from the replacement tmux window. Keep it as a floor until resume
|
|
2477
|
+
// presents a live composer, then rebase to the lower of the pane's actual
|
|
2478
|
+
// count and the seed. Any higher count already includes a new occurrence,
|
|
2479
|
+
// which the normal scan below must still report.
|
|
2480
|
+
if (codexLivePane && this.seededErrorCountBaselines.size > 0) {
|
|
2481
|
+
for (const [key, seed] of this.seededErrorCountBaselines) {
|
|
2482
|
+
const ep = patterns.find(candidate => Daemon.errorPatternKey(candidate) === key);
|
|
2483
|
+
if (ep)
|
|
2484
|
+
this.lastErrorCount.set(key, Math.min(countMatches(ep.pattern), seed));
|
|
2485
|
+
this.seededErrorCountBaselines.delete(key);
|
|
2486
|
+
// Do not let the generic scrolling-count path repeat this rebase if the
|
|
2487
|
+
// live-pane assignment above is removed or changed (#949 mutation guard).
|
|
2488
|
+
rebasedSeededBaselines.add(key);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2443
2491
|
// State: waiting for recovery. A missing/outdated ready pattern must not
|
|
2444
2492
|
// suppress every future error forever, so the gate has a hard deadline.
|
|
2445
2493
|
if (this.errorWaitingForRecovery) {
|
|
@@ -2517,8 +2565,11 @@ export class Daemon extends EventEmitter {
|
|
|
2517
2565
|
if (count <= seen) {
|
|
2518
2566
|
// Occurrences scrolled out of the capture buffer → lower the baseline
|
|
2519
2567
|
// so a future re-occurrence still counts as new (no permanent suppress).
|
|
2520
|
-
|
|
2568
|
+
// A seeded Codex count stays a floor until resume reaches a live composer;
|
|
2569
|
+
// an empty startup frame is not evidence that its history scrolled away.
|
|
2570
|
+
if (count < seen && !this.seededErrorCountBaselines.has(key) && !rebasedSeededBaselines.has(key)) {
|
|
2521
2571
|
this.lastErrorCount.set(key, count);
|
|
2572
|
+
}
|
|
2522
2573
|
continue;
|
|
2523
2574
|
}
|
|
2524
2575
|
// count > seen → a NEW occurrence of this exact pattern appeared.
|
|
@@ -3211,7 +3262,7 @@ export class Daemon extends EventEmitter {
|
|
|
3211
3262
|
const turn = this.turnReplyGuard.snapshot();
|
|
3212
3263
|
if (!turn || this.isPaused)
|
|
3213
3264
|
return;
|
|
3214
|
-
if (turn.
|
|
3265
|
+
if (turn.completionDelivered) {
|
|
3215
3266
|
if (turn.phase === "recovering") {
|
|
3216
3267
|
this.emit("reply_drop_recovered", {
|
|
3217
3268
|
name: this.name,
|
|
@@ -3269,9 +3320,9 @@ export class Daemon extends EventEmitter {
|
|
|
3269
3320
|
return;
|
|
3270
3321
|
}
|
|
3271
3322
|
}
|
|
3272
|
-
// Preserve the dead-MCP proxy path and its explicit opt-in.
|
|
3273
|
-
//
|
|
3274
|
-
//
|
|
3323
|
+
// Preserve the dead-MCP proxy path and its explicit opt-in. Any verified
|
|
3324
|
+
// outbound action suppresses a duplicate proxy; only a delivered
|
|
3325
|
+
// human-facing channel action satisfies the completion guard below.
|
|
3275
3326
|
if (!turn.outboundDelivered && this.config.mcp_proxy_reply === true
|
|
3276
3327
|
&& mcpServerState(this.instanceDir).state === "dead") {
|
|
3277
3328
|
this.turnReplyGuard.complete(turn.generation);
|
|
@@ -3331,7 +3382,7 @@ export class Daemon extends EventEmitter {
|
|
|
3331
3382
|
const current = this.turnReplyGuard.snapshot();
|
|
3332
3383
|
if (!current || current.generation !== turn.generation || current.phase !== "recovering")
|
|
3333
3384
|
return;
|
|
3334
|
-
if (current.
|
|
3385
|
+
if (current.completionDelivered) {
|
|
3335
3386
|
this.turnReplyGuard.complete(turn.generation);
|
|
3336
3387
|
this.emit("reply_drop_recovered", {
|
|
3337
3388
|
name: this.name,
|
|
@@ -4204,11 +4255,7 @@ export class Daemon extends EventEmitter {
|
|
|
4204
4255
|
this.recordRecentUserMessage(content, meta);
|
|
4205
4256
|
const formatted = "[STEERING — mid-task course correction. Fold this into the CURRENT work if one is active.]\n"
|
|
4206
4257
|
+ this.formatInboundMessage(content, meta);
|
|
4207
|
-
const
|
|
4208
|
-
const messageId = meta.message_id;
|
|
4209
|
-
const status = (chatId && messageId)
|
|
4210
|
-
? { chatId: meta.thread_id || chatId, messageId }
|
|
4211
|
-
: undefined;
|
|
4258
|
+
const status = channelStatus(meta);
|
|
4212
4259
|
this.steerLock = this.steerLock.then(async () => {
|
|
4213
4260
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
4214
4261
|
return;
|
|
@@ -4246,11 +4293,7 @@ export class Daemon extends EventEmitter {
|
|
|
4246
4293
|
this.recordRecentUserMessage(content, meta);
|
|
4247
4294
|
const formatted = "[BTW — side question from the user. Answer it separately without changing or interrupting the CURRENT work.]\n"
|
|
4248
4295
|
+ this.formatInboundMessage(content, meta);
|
|
4249
|
-
const
|
|
4250
|
-
const messageId = meta.message_id;
|
|
4251
|
-
const status = (chatId && messageId)
|
|
4252
|
-
? { chatId: meta.thread_id || chatId, messageId }
|
|
4253
|
-
: undefined;
|
|
4296
|
+
const status = channelStatus(meta);
|
|
4254
4297
|
this.steerLock = this.steerLock.then(async () => {
|
|
4255
4298
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
4256
4299
|
return;
|
|
@@ -4327,7 +4370,9 @@ export class Daemon extends EventEmitter {
|
|
|
4327
4370
|
this.logger.warn({ depth: this.pasteQueueDepth }, "Message delivery queue backing up");
|
|
4328
4371
|
}
|
|
4329
4372
|
if (wasQueued && chatId && messageId) {
|
|
4330
|
-
|
|
4373
|
+
const queuedStatus = channelStatus(meta);
|
|
4374
|
+
if (queuedStatus)
|
|
4375
|
+
this.emit("message_queued", queuedStatus);
|
|
4331
4376
|
}
|
|
4332
4377
|
this.pasteLock = this.pasteLock.then(async () => {
|
|
4333
4378
|
try {
|
|
@@ -4346,9 +4391,7 @@ export class Daemon extends EventEmitter {
|
|
|
4346
4391
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
4347
4392
|
return;
|
|
4348
4393
|
}
|
|
4349
|
-
const status = (
|
|
4350
|
-
? { chatId: meta.thread_id || chatId, messageId }
|
|
4351
|
-
: undefined;
|
|
4394
|
+
const status = channelStatus(meta);
|
|
4352
4395
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
4353
4396
|
// the previous turn's tool list.
|
|
4354
4397
|
this.resetToolProgress();
|
|
@@ -6050,14 +6093,14 @@ export class Daemon extends EventEmitter {
|
|
|
6050
6093
|
// an unmapped socket is retained for legacy/tests where mcp_ready was not
|
|
6051
6094
|
// observed before the first tool call.
|
|
6052
6095
|
const sourceSession = this.socketSessionNames.get(socket);
|
|
6053
|
-
const replyAttempt =
|
|
6054
|
-
? this.turnReplyGuard.beginToolAttempt(tool === "reply")
|
|
6096
|
+
const replyAttempt = TURN_OUTBOUND_TOOLS.has(tool) && (!sourceSession || sourceSession === this.name)
|
|
6097
|
+
? this.turnReplyGuard.beginToolAttempt(tool === "reply", TURN_COMPLETION_TOOLS.has(tool))
|
|
6055
6098
|
: null;
|
|
6056
6099
|
// For now, log and respond. Full adapter routing will be wired in fleet manager.
|
|
6057
6100
|
const respond = (result, error) => {
|
|
6058
6101
|
// A message that verifiably went out stands down the dead-MCP proxy reply
|
|
6059
6102
|
// for this turn: the agent proved it can still speak for itself.
|
|
6060
|
-
if (!error && result != null &&
|
|
6103
|
+
if (!error && result != null && TURN_OUTBOUND_TOOLS.has(tool)) {
|
|
6061
6104
|
this.turnReplyGuard.settleToolAttempt(replyAttempt, true);
|
|
6062
6105
|
}
|
|
6063
6106
|
const sent = this.ipcServer?.send(socket, { requestId, result, error }) ?? false;
|