@agentconnect.md/daemon 1.0.0-rc.322 → 1.0.0-rc.324
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/{acp-host-Dsm0i9DH.js → acp-host-DKkoWiWe.js} +70 -4
- package/dist/acp-host-DKkoWiWe.js.map +1 -0
- package/dist/{agent-list-Cs_EgUh5.js → agent-list-CE6JNwIB.js} +2 -2
- package/dist/{agent-list-Cs_EgUh5.js.map → agent-list-CE6JNwIB.js.map} +1 -1
- package/dist/{chat-Cq-5NzYK.js → chat-lv_zfcAK.js} +3 -3
- package/dist/{chat-Cq-5NzYK.js.map → chat-lv_zfcAK.js.map} +1 -1
- package/dist/{dist-BG425zHm.js → dist-D97dgR66.js} +213 -8
- package/dist/dist-D97dgR66.js.map +1 -0
- package/dist/{gh-token-VZeW2R2c.js → gh-token-CYlbctQZ.js} +2 -2
- package/dist/{gh-token-VZeW2R2c.js.map → gh-token-CYlbctQZ.js.map} +1 -1
- package/dist/index.js +5 -5
- package/dist/{load-agents-CROyMyq9.js → load-agents-CuXy_Ei1.js} +3 -2
- package/dist/load-agents-CuXy_Ei1.js.map +1 -0
- package/dist/{login-CaCl6BSv.js → login-BMPemxkh.js} +4 -4
- package/dist/{login-CaCl6BSv.js.map → login-BMPemxkh.js.map} +1 -1
- package/dist/{run-foreground-uU2keIfz.js → run-foreground-BLhVJZlp.js} +1108 -92
- package/dist/{run-foreground-uU2keIfz.js.map → run-foreground-BLhVJZlp.js.map} +1 -1
- package/dist/run-foreground-DhYjpWCl.js +2 -0
- package/package.json +1 -1
- package/dist/acp-host-Dsm0i9DH.js.map +0 -1
- package/dist/dist-BG425zHm.js.map +0 -1
- package/dist/load-agents-CROyMyq9.js.map +0 -1
- package/dist/run-foreground-BRZnFf4F.js +0 -2
|
@@ -3,10 +3,10 @@ import { n as require_ms, t as require_src$3 } from "./src-DDtlVutj.js";
|
|
|
3
3
|
import { n as init_esm_shims, t as __dirname } from "./esm-shims-DxGcm4Hw.js";
|
|
4
4
|
import { t as DAEMON_VERSION } from "./version-DGaRXjBK.js";
|
|
5
5
|
import { C as _coercedNumber, T as NEVER, a as array, d as literal, f as looseObject, i as any, m as object, o as boolean, p as number$1, t as ZodNumber, v as string, x as url$4 } from "./schemas-CQkO9hC5.js";
|
|
6
|
-
import { a as RELAY_DAEMON_WS_PATH, c as SHARED_CONFIG_ACTION_ID, d as encode, f as MAX_FRAME_BYTES, g as
|
|
7
|
-
import { a as agentChildEnv, c as persistRelays, i as selectAgent, n as findAgentFiles, o as loadConfig, r as loadAgents, s as persistDaemonId } from "./load-agents-
|
|
6
|
+
import { _ as ZodIssueCode, a as RELAY_DAEMON_WS_PATH, c as SHARED_CONFIG_ACTION_ID, d as encode, f as MAX_FRAME_BYTES, g as RESERVED_MCP_SERVER_NAME, i as RELAY_DAEMON_SUBPROTOCOL, l as buildEnvelope, m as HookReport, n as gitRepoLabel, o as buildRelayDaemonFrame, r as normalizeGitUrl, s as decodeRelayDaemonFrame, u as decodeEnvelope } from "./dist-D97dgR66.js";
|
|
7
|
+
import { a as agentChildEnv, c as persistRelays, i as selectAgent, n as findAgentFiles, o as loadConfig, r as loadAgents, s as persistDaemonId } from "./load-agents-CuXy_Ei1.js";
|
|
8
8
|
import { l as resolveRoot, o as mcpSocketPath, u as statePath } from "./paths-DEcUO8lC.js";
|
|
9
|
-
import { C as
|
|
9
|
+
import { C as cachedRuntimeNames, S as toolsForIntegrations, T as resolveRuntimes, _ as resolveInMemoryDir, a as prepareWorkspace, b as GITHUB_REVIEW_TOOLS, c as sessionGitEnv, d as memoryKindOf, f as memoryProviderFor, g as listMemory, h as MemoryTooLargeError, i as prefetchWorkspace, l as simpleGit, m as MemoryPathError, n as turnFailureReason, o as initGitInjection, p as MemoryConflictError, r as installedRuntimes, s as probeGitVersion, t as AcpHost, u as createMemoryProvider, v as writeMemoryFile, w as cachedRuntimeVersions, x as MEMORY_TOOL_NAMES, y as ALL_TOOL_NAMES } from "./acp-host-DKkoWiWe.js";
|
|
10
10
|
import { i as GitCredentialCache, n as gitcredSocketPath, r as writeGitcredShim, t as GitCredServer } from "./gitcred-server-Bu-Svl7L.js";
|
|
11
11
|
import { B as ToolListChangedNotificationSchema, D as ListPromptsResultSchema, E as ListChangedOptionsBaseSchema, F as PromptListChangedNotificationSchema, G as isZ4Schema, H as isJSONRPCRequest, I as ReadResourceResultSchema, J as encodeFrame, K as safeParse, L as ResourceListChangedNotificationSchema, M as ListToolsResultSchema, O as ListResourceTemplatesResultSchema, P as McpError, R as SUPPORTED_PROTOCOL_VERSIONS, S as InitializeResultSchema, T as LATEST_PROTOCOL_VERSION, U as isJSONRPCResultResponse, V as isInitializedNotification, W as getObjectShape, _ as ElicitResultSchema, a as AjvJsonSchemaValidator, b as GetPromptResultSchema, c as mergeCapabilities, d as CompleteResultSchema, f as CreateMessageRequestSchema, g as ElicitRequestSchema, h as CreateTaskResultSchema, i as assertToolsCallTaskCapability, k as ListResourcesResultSchema, m as CreateMessageResultWithToolsSchema, n as serializeMessage, o as require_fast_deep_equal, p as CreateMessageResultSchema, q as decodeFrames, r as assertClientRequestTaskCapability, s as Protocol, t as ReadBuffer, u as CallToolResultSchema, v as EmptyResultSchema, w as JSONRPCMessageSchema, y as ErrorCode } from "./stdio-DnS8WcXi.js";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
@@ -4229,6 +4229,7 @@ var CpClient = class {
|
|
|
4229
4229
|
correlator;
|
|
4230
4230
|
stopped = false;
|
|
4231
4231
|
fatal = false;
|
|
4232
|
+
hookReportAckV1 = false;
|
|
4232
4233
|
attempt = 0;
|
|
4233
4234
|
reconnectTimer;
|
|
4234
4235
|
lastAuthedEpoch = 0;
|
|
@@ -4306,12 +4307,14 @@ var CpClient = class {
|
|
|
4306
4307
|
this.deps.onWebAppUrl?.(ok.webAppUrl);
|
|
4307
4308
|
this.deps.onOrgSlug?.(ok.orgSlug);
|
|
4308
4309
|
this.state = "REGISTERING";
|
|
4309
|
-
const
|
|
4310
|
+
const regOk = await this.request("register", {
|
|
4310
4311
|
host: this.deps.host,
|
|
4311
4312
|
capabilities: this.deps.capabilities(),
|
|
4312
4313
|
maxAgents: this.deps.maxAgents,
|
|
4313
4314
|
localState: this.deps.localState()
|
|
4314
|
-
})
|
|
4315
|
+
});
|
|
4316
|
+
const snap = regOk.payload;
|
|
4317
|
+
this.hookReportAckV1 = regOk.payload.serverFeatures?.includes("hook-report-ack-v1") ?? false;
|
|
4315
4318
|
this.routingEpoch = snap.routingEpoch;
|
|
4316
4319
|
this.deps.configApply.applyReconcileSnapshot(snap);
|
|
4317
4320
|
this.state = "READY";
|
|
@@ -4380,15 +4383,45 @@ var CpClient = class {
|
|
|
4380
4383
|
this.transport?.send(encode(buildEnvelope("cron/report", report)));
|
|
4381
4384
|
}
|
|
4382
4385
|
/**
|
|
4383
|
-
*
|
|
4384
|
-
*
|
|
4385
|
-
*
|
|
4386
|
-
*
|
|
4387
|
-
* No-op unless READY/DRAINING.
|
|
4386
|
+
* Durably converge a hook turn outcome (D→C `hook/report` REQ). The daemon
|
|
4387
|
+
* retains its metadata-only outbox row until the CP replies after persistence
|
|
4388
|
+
* and projection convergence, so a long CP outage delays GC instead of losing
|
|
4389
|
+
* a terminal result.
|
|
4388
4390
|
*/
|
|
4389
|
-
emitHookReport(report) {
|
|
4390
|
-
|
|
4391
|
-
this.
|
|
4391
|
+
async emitHookReport(report) {
|
|
4392
|
+
this.requireReady("hook/report");
|
|
4393
|
+
if (!this.hookReportAckV1) {
|
|
4394
|
+
this.transport.send(encode(buildEnvelope("hook/report", report)));
|
|
4395
|
+
return "legacy-sent";
|
|
4396
|
+
}
|
|
4397
|
+
const rep = await this.request("hook/report", report);
|
|
4398
|
+
if (rep.type !== "ack" || rep.payload.ok !== true) throw new WireError("INTERNAL", `expected hook/report ack, got ${rep.type}`, false);
|
|
4399
|
+
return "acknowledged";
|
|
4400
|
+
}
|
|
4401
|
+
/** Durable start barrier for an accepted GitHub hook turn. Formal review is
|
|
4402
|
+
* not exposed until this correlated request succeeds. */
|
|
4403
|
+
async startHook(payload) {
|
|
4404
|
+
this.requireReady("hook/start");
|
|
4405
|
+
const rep = await this.request("hook/start", payload);
|
|
4406
|
+
if (rep.type !== "hook/start/ok") throw new WireError("INTERNAL", `expected hook/start/ok, got ${rep.type}`, false);
|
|
4407
|
+
return rep.payload;
|
|
4408
|
+
}
|
|
4409
|
+
/** One-attempt, action-time formal-review purpose token. */
|
|
4410
|
+
async authorizeGithubReview(payload) {
|
|
4411
|
+
this.requireReady("github/review-authorize");
|
|
4412
|
+
const rep = await this.request("github/review-authorize", payload);
|
|
4413
|
+
if (rep.type !== "github/review-authorized") throw new WireError("INTERNAL", `expected github/review-authorized, got ${rep.type}`, false);
|
|
4414
|
+
return rep.payload;
|
|
4415
|
+
}
|
|
4416
|
+
/** Immediate body-free outcome; HookReport repeats it for lost-reply recovery. */
|
|
4417
|
+
async reportGithubReviewResult(payload) {
|
|
4418
|
+
this.requireReady("github/review-result");
|
|
4419
|
+
const rep = await this.request("github/review-result", payload);
|
|
4420
|
+
if (rep.type !== "github/review-result/ok") throw new WireError("INTERNAL", `expected github/review-result/ok, got ${rep.type}`, false);
|
|
4421
|
+
return rep.payload;
|
|
4422
|
+
}
|
|
4423
|
+
requireReady(op) {
|
|
4424
|
+
if (this.state !== "READY" && this.state !== "DRAINING" || !this.transport) throw new WireError("INTERNAL", `control plane unreachable for ${op} (client ${this.state})`, true);
|
|
4392
4425
|
}
|
|
4393
4426
|
request(type, payload) {
|
|
4394
4427
|
const frame = buildEnvelope(type, payload);
|
|
@@ -6416,6 +6449,11 @@ var LocalStore = class {
|
|
|
6416
6449
|
msg TEXT NOT NULL,
|
|
6417
6450
|
integrationId TEXT,
|
|
6418
6451
|
callMeta TEXT,
|
|
6452
|
+
hookContext TEXT,
|
|
6453
|
+
posterPublishState TEXT,
|
|
6454
|
+
terminalReport TEXT,
|
|
6455
|
+
legacyReportConnection TEXT,
|
|
6456
|
+
completedAt INTEGER,
|
|
6419
6457
|
isQueueCmd INTEGER,
|
|
6420
6458
|
loopGuardCounted INTEGER NOT NULL DEFAULT 0,
|
|
6421
6459
|
enqueuedAt TEXT NOT NULL
|
|
@@ -6481,6 +6519,7 @@ var LocalStore = class {
|
|
|
6481
6519
|
this.migrateInboxLoopGuardCounted();
|
|
6482
6520
|
this.migrateTranscriptToolBody();
|
|
6483
6521
|
this.migrateTranscriptRecipient();
|
|
6522
|
+
this.migrateInboxHookContext();
|
|
6484
6523
|
}
|
|
6485
6524
|
/** Add the `transcript.recipient` column (the agent a row was delivered to) to a
|
|
6486
6525
|
* pre-existing DB. Legacy rows have NULL recipient, so they surface in a session view
|
|
@@ -6488,6 +6527,16 @@ var LocalStore = class {
|
|
|
6488
6527
|
migrateTranscriptRecipient() {
|
|
6489
6528
|
if (!this.db.prepare("PRAGMA table_info(transcript)").all().some((c) => c.name === "recipient")) this.db.exec("ALTER TABLE transcript ADD COLUMN recipient TEXT");
|
|
6490
6529
|
}
|
|
6530
|
+
/** R1 hook turns must retain their trusted dispatch fence and the poster's
|
|
6531
|
+
* single-attempt state across restart. Upgrade existing local stores in place. */
|
|
6532
|
+
migrateInboxHookContext() {
|
|
6533
|
+
const cols = this.db.prepare("PRAGMA table_info(inbox)").all();
|
|
6534
|
+
if (!cols.some((c) => c.name === "hookContext")) this.db.exec("ALTER TABLE inbox ADD COLUMN hookContext TEXT");
|
|
6535
|
+
if (!cols.some((c) => c.name === "posterPublishState")) this.db.exec("ALTER TABLE inbox ADD COLUMN posterPublishState TEXT");
|
|
6536
|
+
if (!cols.some((c) => c.name === "terminalReport")) this.db.exec("ALTER TABLE inbox ADD COLUMN terminalReport TEXT");
|
|
6537
|
+
if (!cols.some((c) => c.name === "legacyReportConnection")) this.db.exec("ALTER TABLE inbox ADD COLUMN legacyReportConnection TEXT");
|
|
6538
|
+
if (!cols.some((c) => c.name === "completedAt")) this.db.exec("ALTER TABLE inbox ADD COLUMN completedAt INTEGER");
|
|
6539
|
+
}
|
|
6491
6540
|
/** Backfill the durable mute tombstones from the legacy sessions.muted column.
|
|
6492
6541
|
* Both writes in setSessionMuted stay transactional, so this idempotent sync cannot
|
|
6493
6542
|
* resurrect a mute that was explicitly cleared. */
|
|
@@ -6930,28 +6979,96 @@ var LocalStore = class {
|
|
|
6930
6979
|
return this.db.prepare("SELECT lastRunAt FROM cron_runs WHERE key = ?").get(key)?.lastRunAt;
|
|
6931
6980
|
}
|
|
6932
6981
|
/** §6.9 #353 durable inbox: persist an admitted message BEFORE its admission ACK. Keyed
|
|
6933
|
-
* by the stable id (deliveryId/msgId)
|
|
6982
|
+
* by the stable id (deliveryId/msgId). A re-append preserves the original payload/FIFO
|
|
6934
6983
|
* position and may only advance the durable loop-accounting marker from 0 to 1. */
|
|
6935
6984
|
appendInbox(row) {
|
|
6936
|
-
this.db.prepare(`INSERT INTO inbox
|
|
6937
|
-
|
|
6985
|
+
const inserted = this.db.prepare(`INSERT OR IGNORE INTO inbox
|
|
6986
|
+
(id, sessionKey, agentId, msg, integrationId, callMeta, hookContext, posterPublishState,
|
|
6987
|
+
terminalReport, legacyReportConnection, completedAt, isQueueCmd, loopGuardCounted, enqueuedAt)
|
|
6938
6988
|
VALUES
|
|
6939
|
-
(@id, @sessionKey, @agentId, @msg, @integrationId, @callMeta, @
|
|
6940
|
-
|
|
6941
|
-
loopGuardCounted=MAX(inbox.loopGuardCounted, excluded.loopGuardCounted)`).run({
|
|
6989
|
+
(@id, @sessionKey, @agentId, @msg, @integrationId, @callMeta, @hookContext, @posterPublishState,
|
|
6990
|
+
@terminalReport, @legacyReportConnection, @completedAt, @isQueueCmd, @loopGuardCounted, @enqueuedAt)`).run({
|
|
6942
6991
|
id: row.id,
|
|
6943
6992
|
sessionKey: row.sessionKey,
|
|
6944
6993
|
agentId: row.agentId,
|
|
6945
6994
|
msg: row.msg,
|
|
6946
6995
|
integrationId: row.integrationId ?? null,
|
|
6947
6996
|
callMeta: row.callMeta ?? null,
|
|
6997
|
+
hookContext: row.hookContext ?? null,
|
|
6998
|
+
posterPublishState: row.posterPublishState ?? null,
|
|
6999
|
+
terminalReport: row.terminalReport ?? null,
|
|
7000
|
+
legacyReportConnection: row.legacyReportConnection ?? null,
|
|
7001
|
+
completedAt: row.completedAt ?? null,
|
|
6948
7002
|
isQueueCmd: row.isQueueCmd ?? null,
|
|
6949
7003
|
loopGuardCounted: row.loopGuardCounted ?? 0,
|
|
6950
7004
|
enqueuedAt: row.enqueuedAt
|
|
6951
7005
|
});
|
|
7006
|
+
if (inserted.changes === 0 && row.loopGuardCounted === 1) this.db.prepare("UPDATE inbox SET loopGuardCounted = MAX(loopGuardCounted, 1) WHERE id = ?").run(row.id);
|
|
7007
|
+
return inserted.changes === 1;
|
|
6952
7008
|
}
|
|
6953
|
-
/**
|
|
6954
|
-
*
|
|
7009
|
+
/** Stable-id admission probe used before any hook anchoring side effect. A
|
|
7010
|
+
* live row will be replayed (or is already running); a terminal row is the
|
|
7011
|
+
* durable receipt. Either way, redelivery must not post another anchor. */
|
|
7012
|
+
hasInbox(id) {
|
|
7013
|
+
return this.db.prepare("SELECT 1 FROM inbox WHERE id = ?").get(id) !== void 0;
|
|
7014
|
+
}
|
|
7015
|
+
updateInboxHookState(id, hookContext, posterPublishState) {
|
|
7016
|
+
return this.db.prepare(`UPDATE inbox
|
|
7017
|
+
SET hookContext = @hookContext,
|
|
7018
|
+
posterPublishState = COALESCE(@posterPublishState, posterPublishState)
|
|
7019
|
+
WHERE id = @id`).run({
|
|
7020
|
+
id,
|
|
7021
|
+
hookContext,
|
|
7022
|
+
posterPublishState: posterPublishState ?? null
|
|
7023
|
+
}).changes === 1;
|
|
7024
|
+
}
|
|
7025
|
+
/** Atomically turn a live hook inbox row into a redacted terminal receipt.
|
|
7026
|
+
* The stable id remains present to absorb relay redelivery after restart;
|
|
7027
|
+
* startup re-emits only the metadata report, never the model prompt. The CAS
|
|
7028
|
+
* result identifies the sole writer so a later terminal path cannot replace
|
|
7029
|
+
* the winning outbox body. */
|
|
7030
|
+
completeHookInbox(id, terminalReport, completedAt) {
|
|
7031
|
+
if (this.db.prepare(`UPDATE inbox
|
|
7032
|
+
SET msg = '{}', integrationId = NULL, callMeta = NULL, hookContext = NULL,
|
|
7033
|
+
posterPublishState = 'settled', terminalReport = @terminalReport,
|
|
7034
|
+
legacyReportConnection = NULL, completedAt = @completedAt, isQueueCmd = NULL
|
|
7035
|
+
WHERE id = @id AND hookContext IS NOT NULL AND completedAt IS NULL`).run({
|
|
7036
|
+
id,
|
|
7037
|
+
terminalReport,
|
|
7038
|
+
completedAt
|
|
7039
|
+
}).changes === 1) return "completed";
|
|
7040
|
+
const row = this.db.prepare("SELECT completedAt FROM inbox WHERE id = ?").get(id);
|
|
7041
|
+
return row?.completedAt !== null && row?.completedAt !== void 0 ? "already-terminal" : "missing";
|
|
7042
|
+
}
|
|
7043
|
+
/** Remember a best-effort legacy EVT on this exact CP connection so an
|
|
7044
|
+
* outbox pump can move past it without repeatedly re-sending. The report body
|
|
7045
|
+
* remains retained until a future ACK-capable connection durably converges it. */
|
|
7046
|
+
markLegacyHookReportSent(id, connectionId) {
|
|
7047
|
+
return this.db.prepare(`UPDATE inbox
|
|
7048
|
+
SET legacyReportConnection = @connectionId
|
|
7049
|
+
WHERE id = @id AND terminalReport IS NOT NULL`).run({
|
|
7050
|
+
id,
|
|
7051
|
+
connectionId
|
|
7052
|
+
}).changes === 1;
|
|
7053
|
+
}
|
|
7054
|
+
/** A CP-correlated ACK releases only the report payload. Keep a bounded
|
|
7055
|
+
* metadata-only stable-id receipt so relay redelivery still cannot rerun the
|
|
7056
|
+
* model; unacknowledged reports are never capacity-evicted. */
|
|
7057
|
+
acknowledgeHookInbox(id, maxAcknowledgedReceipts = 1e4) {
|
|
7058
|
+
const result = this.db.prepare(`UPDATE inbox
|
|
7059
|
+
SET terminalReport = NULL, legacyReportConnection = NULL
|
|
7060
|
+
WHERE id = @id AND completedAt IS NOT NULL AND terminalReport IS NOT NULL`).run({ id });
|
|
7061
|
+
if (result.changes === 1) this.db.prepare(`DELETE FROM inbox
|
|
7062
|
+
WHERE id IN (
|
|
7063
|
+
SELECT id FROM inbox
|
|
7064
|
+
WHERE completedAt IS NOT NULL AND terminalReport IS NULL
|
|
7065
|
+
ORDER BY completedAt DESC, id DESC
|
|
7066
|
+
LIMIT -1 OFFSET @maxAcknowledgedReceipts
|
|
7067
|
+
)`).run({ maxAcknowledgedReceipts });
|
|
7068
|
+
return result.changes === 1;
|
|
7069
|
+
}
|
|
7070
|
+
/** Remove an ordinary inbox row once its turn reaches a terminal state. Hook
|
|
7071
|
+
* rows are converted to bounded redacted receipts by completeHookInbox. */
|
|
6955
7072
|
removeInbox(id) {
|
|
6956
7073
|
this.db.prepare("DELETE FROM inbox WHERE id = ?").run(id);
|
|
6957
7074
|
}
|
|
@@ -6960,15 +7077,15 @@ var LocalStore = class {
|
|
|
6960
7077
|
listInboxBySessionKeyFifo() {
|
|
6961
7078
|
return this.db.prepare("SELECT * FROM inbox ORDER BY sessionKey ASC, enqueuedAt ASC").all();
|
|
6962
7079
|
}
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
*
|
|
6968
|
-
* restart. Returns the removed ids so the daemon can clear its live-id cache. */
|
|
7080
|
+
/** Remove ordinary durable turns owned by an agent. Live hook rows have their
|
|
7081
|
+
* own single completion owner and must survive until that owner atomically
|
|
7082
|
+
* redacts them into a terminal report. Unacknowledged terminal hook reports
|
|
7083
|
+
* are an outbox and likewise must survive lifecycle purges. ACKed receipts
|
|
7084
|
+
* may be discarded here because CP already durably converged them. */
|
|
6969
7085
|
removeInboxByAgentId(agentId) {
|
|
6970
|
-
const
|
|
6971
|
-
this.db.prepare(
|
|
7086
|
+
const removable = "agentId = ? AND hookContext IS NULL AND terminalReport IS NULL";
|
|
7087
|
+
const rows = this.db.prepare(`SELECT id FROM inbox WHERE ${removable}`).all(agentId);
|
|
7088
|
+
this.db.prepare(`DELETE FROM inbox WHERE ${removable}`).run(agentId);
|
|
6972
7089
|
return rows.map((row) => row.id);
|
|
6973
7090
|
}
|
|
6974
7091
|
/** Record one turn admission against a conversation-wide fixed window. A trusted
|
|
@@ -7692,6 +7809,31 @@ async function executeTool(ctx, name, args, deps) {
|
|
|
7692
7809
|
cancelled: true
|
|
7693
7810
|
};
|
|
7694
7811
|
}
|
|
7812
|
+
if (name === "submitGithubReview") {
|
|
7813
|
+
if (!deps.submitGithubReview) throw new Error("formal GitHub reviews are unavailable on this daemon");
|
|
7814
|
+
const event = requireEnum(args, "event", [
|
|
7815
|
+
"COMMENT",
|
|
7816
|
+
"REQUEST_CHANGES",
|
|
7817
|
+
"APPROVE"
|
|
7818
|
+
]);
|
|
7819
|
+
const verdict = requireEnum(args, "verdict", [
|
|
7820
|
+
"pass",
|
|
7821
|
+
"fail",
|
|
7822
|
+
"neutral"
|
|
7823
|
+
]);
|
|
7824
|
+
const body = requireStringAllowEmpty(args, "body");
|
|
7825
|
+
const comments = parseReviewComments(args.comments);
|
|
7826
|
+
return deps.submitGithubReview({
|
|
7827
|
+
agentId: ctx.agentId,
|
|
7828
|
+
platform: ctx.platform,
|
|
7829
|
+
channel: ctx.channel,
|
|
7830
|
+
thread: ctx.thread,
|
|
7831
|
+
event,
|
|
7832
|
+
verdict,
|
|
7833
|
+
body,
|
|
7834
|
+
...comments ? { comments } : {}
|
|
7835
|
+
});
|
|
7836
|
+
}
|
|
7695
7837
|
const gw = ctx.integrationId ? deps.gatewayFor(ctx.integrationId) : void 0;
|
|
7696
7838
|
if (!gw) throw new Error(`no live platform connection for integration ${ctx.integrationId ?? "(none)"}`);
|
|
7697
7839
|
switch (name) {
|
|
@@ -7778,6 +7920,35 @@ function optionalNumber(args, key) {
|
|
|
7778
7920
|
if (typeof v !== "number" || !Number.isFinite(v)) throw new Error(`argument ${key} must be a finite number`);
|
|
7779
7921
|
return v;
|
|
7780
7922
|
}
|
|
7923
|
+
function requireEnum(args, key, values) {
|
|
7924
|
+
const value = args[key];
|
|
7925
|
+
if (typeof value !== "string" || !values.includes(value)) throw new Error(`argument ${key} must be one of: ${values.join(", ")}`);
|
|
7926
|
+
return value;
|
|
7927
|
+
}
|
|
7928
|
+
function requirePositiveInt(args, key) {
|
|
7929
|
+
const value = args[key];
|
|
7930
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new Error(`argument ${key} must be a positive integer`);
|
|
7931
|
+
return value;
|
|
7932
|
+
}
|
|
7933
|
+
function parseReviewComments(value) {
|
|
7934
|
+
if (value === void 0 || value === null) return void 0;
|
|
7935
|
+
if (!Array.isArray(value)) throw new Error("argument comments must be an array");
|
|
7936
|
+
if (value.length > 100) throw new Error("argument comments may contain at most 100 entries");
|
|
7937
|
+
return value.map((item, index) => {
|
|
7938
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) throw new Error(`comments[${index}] must be an object`);
|
|
7939
|
+
const row = item;
|
|
7940
|
+
const startLine = row.startLine === void 0 ? void 0 : requirePositiveInt(row, "startLine");
|
|
7941
|
+
const startSide = row.startSide === void 0 ? void 0 : requireEnum(row, "startSide", ["LEFT", "RIGHT"]);
|
|
7942
|
+
return {
|
|
7943
|
+
path: requireString(row, "path"),
|
|
7944
|
+
body: requireString(row, "body"),
|
|
7945
|
+
line: requirePositiveInt(row, "line"),
|
|
7946
|
+
side: requireEnum(row, "side", ["LEFT", "RIGHT"]),
|
|
7947
|
+
...startLine !== void 0 ? { startLine } : {},
|
|
7948
|
+
...startSide !== void 0 ? { startSide } : {}
|
|
7949
|
+
};
|
|
7950
|
+
});
|
|
7951
|
+
}
|
|
7781
7952
|
//#endregion
|
|
7782
7953
|
//#region src/mcp/control-server.ts
|
|
7783
7954
|
/**
|
|
@@ -11251,7 +11422,7 @@ var SSEClientTransport = class {
|
|
|
11251
11422
|
};
|
|
11252
11423
|
//#endregion
|
|
11253
11424
|
//#region src/mcp/mcp-prober.ts
|
|
11254
|
-
const DEFAULT_TIMEOUT_MS$
|
|
11425
|
+
const DEFAULT_TIMEOUT_MS$2 = 15e3;
|
|
11255
11426
|
const DEFAULT_CONCURRENCY$1 = 3;
|
|
11256
11427
|
function envRecord(pairs) {
|
|
11257
11428
|
return Object.fromEntries(pairs.map((e) => [e.name, e.value]));
|
|
@@ -11290,7 +11461,7 @@ async function connectReal(name, def) {
|
|
|
11290
11461
|
* not a failure.
|
|
11291
11462
|
*/
|
|
11292
11463
|
async function probeMcpServer(name, def, opts = {}) {
|
|
11293
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS$
|
|
11464
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS$2;
|
|
11294
11465
|
let timer;
|
|
11295
11466
|
let timedOut = false;
|
|
11296
11467
|
const timeout = new Promise((_, reject) => {
|
|
@@ -197666,7 +197837,10 @@ const UNTRUSTED_CONTENT_BEGIN = "----- BEGIN UNTRUSTED EXTERNAL CONTENT (GitHub
|
|
|
197666
197837
|
const UNTRUSTED_CONTENT_END = "----- END UNTRUSTED EXTERNAL CONTENT -----";
|
|
197667
197838
|
/** channel/thread from the affinity key — see the sessionKey grammar on {@link RdMsgHook}. */
|
|
197668
197839
|
function splitSessionKey(msg) {
|
|
197669
|
-
if (msg.sessionKey === msg.hookId) return {
|
|
197840
|
+
if (msg.sessionKey === msg.hookId) return {
|
|
197841
|
+
channel: msg.hookId,
|
|
197842
|
+
thread: msg.hookId
|
|
197843
|
+
};
|
|
197670
197844
|
if (msg.sessionKey === `${msg.hookId}:${msg.deliveryKey}`) return {
|
|
197671
197845
|
channel: msg.hookId,
|
|
197672
197846
|
thread: msg.deliveryKey
|
|
@@ -197731,7 +197905,7 @@ function neutralizeDelimiters(body) {
|
|
|
197731
197905
|
* Only emitted when the poster will actually run (a thread number exists); push
|
|
197732
197906
|
* fires have no thread, so they never see this. */
|
|
197733
197907
|
function githubReplyHint(c) {
|
|
197734
|
-
return ["", `Your final reply is posted back to ${`${c.repo ?? "this thread"}#${c.number}`} automatically as
|
|
197908
|
+
return ["", `Your final reply is posted back to ${`${c.repo ?? "this thread"}#${c.number}`} automatically as an ordinary GitHub comment. Keep it self-contained; the daemon exclusively owns that reply comment. If this active PR hook permits a formal review, use only the structured \`submitGithubReview\` tool for COMMENT / REQUEST_CHANGES / APPROVE and inline review comments. Do NOT create, update, or delete GitHub comments or formal reviews through \`gh\`, another CLI, a connector, or a direct API call — those paths would race or double-post. Other GitHub tools are for READ-only inspection (thread, diff, files), then return the ordinary comment content in your final reply.`].join("\n");
|
|
197735
197909
|
}
|
|
197736
197910
|
/** The github-kind turn text: a trusted metadata header + the FENCED excerpt.
|
|
197737
197911
|
* The title rides the header (relay-sanitized to one capped line) — it is
|
|
@@ -198125,6 +198299,227 @@ var GithubFinalPoster = class {
|
|
|
198125
198299
|
}
|
|
198126
198300
|
};
|
|
198127
198301
|
//#endregion
|
|
198302
|
+
//#region src/github/review.ts
|
|
198303
|
+
const DEFAULT_TIMEOUT_MS$1 = 15e3;
|
|
198304
|
+
const MAX_REVIEW_PAGES = 10;
|
|
198305
|
+
const REVIEWS_PER_PAGE = 100;
|
|
198306
|
+
var GithubHttpError = class extends Error {
|
|
198307
|
+
status;
|
|
198308
|
+
constructor(status, message) {
|
|
198309
|
+
super(message);
|
|
198310
|
+
this.status = status;
|
|
198311
|
+
this.name = "GithubHttpError";
|
|
198312
|
+
}
|
|
198313
|
+
};
|
|
198314
|
+
function markerFor(target) {
|
|
198315
|
+
return `<!-- agentconnect-review:${Buffer.from(JSON.stringify({
|
|
198316
|
+
v: 1,
|
|
198317
|
+
hookId: target.hookId,
|
|
198318
|
+
deliveryKey: target.deliveryKey,
|
|
198319
|
+
attemptId: target.attemptId,
|
|
198320
|
+
headSha: target.expectedHeadSha
|
|
198321
|
+
}), "utf8").toString("base64url")} -->`;
|
|
198322
|
+
}
|
|
198323
|
+
function validateInput(input) {
|
|
198324
|
+
if (input.event === "APPROVE" && input.verdict !== "pass") return "APPROVE requires verdict=pass";
|
|
198325
|
+
if (input.event === "REQUEST_CHANGES" && input.verdict !== "fail") return "REQUEST_CHANGES requires verdict=fail";
|
|
198326
|
+
if ((input.event === "COMMENT" || input.event === "REQUEST_CHANGES") && !input.body.trim()) return `${input.event} requires a non-empty body`;
|
|
198327
|
+
for (const [index, comment] of (input.comments ?? []).entries()) {
|
|
198328
|
+
if (!comment.path.trim()) return `comments[${index}].path is required`;
|
|
198329
|
+
if (!comment.body.trim()) return `comments[${index}].body is required`;
|
|
198330
|
+
if (!Number.isInteger(comment.line) || comment.line <= 0) return `comments[${index}].line must be positive`;
|
|
198331
|
+
if (comment.startLine !== void 0) {
|
|
198332
|
+
if (!Number.isInteger(comment.startLine) || comment.startLine <= 0 || comment.startLine > comment.line) return `comments[${index}].startLine must be positive and no greater than line`;
|
|
198333
|
+
if (!comment.startSide) return `comments[${index}].startSide is required with startLine`;
|
|
198334
|
+
}
|
|
198335
|
+
}
|
|
198336
|
+
}
|
|
198337
|
+
function safeJson(text) {
|
|
198338
|
+
if (!text) return void 0;
|
|
198339
|
+
return JSON.parse(text.replace(/"id"\s*:\s*(\d{15,})/g, "\"id\":\"$1\""));
|
|
198340
|
+
}
|
|
198341
|
+
/** One lexical-scope client; it never caches tokens or targets. */
|
|
198342
|
+
var GithubReviewClient = class {
|
|
198343
|
+
fetchImpl;
|
|
198344
|
+
baseUrl;
|
|
198345
|
+
timeoutMs;
|
|
198346
|
+
constructor(deps = {}) {
|
|
198347
|
+
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
198348
|
+
this.baseUrl = (deps.baseUrl ?? "https://api.github.com").replace(/\/+$/, "");
|
|
198349
|
+
this.timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
|
|
198350
|
+
}
|
|
198351
|
+
async getPull(token, repoFullName, pullNumber) {
|
|
198352
|
+
const pull = await this.request(`/repos/${repoFullName}/pulls/${pullNumber}`, token, "GET");
|
|
198353
|
+
const headSha = pull.head?.sha;
|
|
198354
|
+
const baseSha = pull.base?.sha;
|
|
198355
|
+
if (!headSha || !baseSha) throw new GithubHttpError(502, "GitHub pull response omitted head/base SHA");
|
|
198356
|
+
return {
|
|
198357
|
+
headSha,
|
|
198358
|
+
baseSha,
|
|
198359
|
+
draft: pull.draft === true,
|
|
198360
|
+
state: pull.state ?? "unknown",
|
|
198361
|
+
merged: pull.merged === true
|
|
198362
|
+
};
|
|
198363
|
+
}
|
|
198364
|
+
async submit(target, input) {
|
|
198365
|
+
const invalid = validateInput(input);
|
|
198366
|
+
if (invalid) return {
|
|
198367
|
+
state: "not_submitted",
|
|
198368
|
+
code: "invalid_input",
|
|
198369
|
+
message: invalid
|
|
198370
|
+
};
|
|
198371
|
+
const marker = markerFor(target);
|
|
198372
|
+
try {
|
|
198373
|
+
let existing;
|
|
198374
|
+
try {
|
|
198375
|
+
existing = await this.findByMarker(target, marker);
|
|
198376
|
+
} catch (err) {
|
|
198377
|
+
if (target.recovering) return {
|
|
198378
|
+
state: "ambiguous",
|
|
198379
|
+
code: "ambiguous_write",
|
|
198380
|
+
message: "cannot reconcile the prior formal review attempt; automatic retry is blocked"
|
|
198381
|
+
};
|
|
198382
|
+
throw err;
|
|
198383
|
+
}
|
|
198384
|
+
if (existing) return this.submitted(existing, target, input);
|
|
198385
|
+
const pull = await this.getPull(target.token, target.repoFullName, target.pullNumber);
|
|
198386
|
+
if (pull.state !== "open" || pull.merged || pull.draft) return {
|
|
198387
|
+
state: "not_submitted",
|
|
198388
|
+
code: "pull_unavailable",
|
|
198389
|
+
message: `pull request is ${pull.merged ? "merged" : pull.draft ? "draft" : pull.state}`
|
|
198390
|
+
};
|
|
198391
|
+
if (pull.headSha !== target.expectedHeadSha || pull.baseSha !== target.expectedBaseSha) return {
|
|
198392
|
+
state: "not_submitted",
|
|
198393
|
+
code: "revision_changed",
|
|
198394
|
+
message: "pull request head/base changed while the review was running"
|
|
198395
|
+
};
|
|
198396
|
+
const body = input.body.trim() ? `${input.body}\n\n${marker}` : marker;
|
|
198397
|
+
try {
|
|
198398
|
+
const created = await this.request(`/repos/${target.repoFullName}/pulls/${target.pullNumber}/reviews`, target.token, "POST", {
|
|
198399
|
+
commit_id: target.expectedHeadSha,
|
|
198400
|
+
event: input.event,
|
|
198401
|
+
body,
|
|
198402
|
+
...input.comments?.length ? { comments: input.comments.map((comment) => ({
|
|
198403
|
+
path: comment.path,
|
|
198404
|
+
body: comment.body,
|
|
198405
|
+
line: comment.line,
|
|
198406
|
+
side: comment.side,
|
|
198407
|
+
...comment.startLine !== void 0 ? { start_line: comment.startLine } : {},
|
|
198408
|
+
...comment.startSide !== void 0 ? { start_side: comment.startSide } : {}
|
|
198409
|
+
})) } : {}
|
|
198410
|
+
});
|
|
198411
|
+
return this.submitted(created, target, input);
|
|
198412
|
+
} catch (err) {
|
|
198413
|
+
if (err instanceof GithubHttpError && err.status >= 400 && err.status < 500) return {
|
|
198414
|
+
state: "not_submitted",
|
|
198415
|
+
code: "github_rejected",
|
|
198416
|
+
message: err.message
|
|
198417
|
+
};
|
|
198418
|
+
const recovered = await this.findByMarker(target, marker).catch(() => void 0);
|
|
198419
|
+
if (recovered) return this.submitted(recovered, target, input);
|
|
198420
|
+
return {
|
|
198421
|
+
state: "ambiguous",
|
|
198422
|
+
code: "ambiguous_write",
|
|
198423
|
+
message: "GitHub review outcome is unknown; automatic retry is blocked"
|
|
198424
|
+
};
|
|
198425
|
+
}
|
|
198426
|
+
} catch (err) {
|
|
198427
|
+
return {
|
|
198428
|
+
state: "not_submitted",
|
|
198429
|
+
code: "github_rejected",
|
|
198430
|
+
message: err instanceof Error ? err.message : String(err)
|
|
198431
|
+
};
|
|
198432
|
+
}
|
|
198433
|
+
}
|
|
198434
|
+
/** Read-only recovery for an attempt whose POST outcome was ambiguous. This
|
|
198435
|
+
* is safe to run automatically before a replayed model turn: it can converge
|
|
198436
|
+
* a now-visible marker, but never issues a second review mutation. */
|
|
198437
|
+
async reconcile(target, event, verdict) {
|
|
198438
|
+
const marker = markerFor(target);
|
|
198439
|
+
try {
|
|
198440
|
+
const existing = await this.findByMarker(target, marker);
|
|
198441
|
+
if (existing) return this.submitted(existing, target, {
|
|
198442
|
+
event,
|
|
198443
|
+
verdict,
|
|
198444
|
+
body: ""
|
|
198445
|
+
});
|
|
198446
|
+
return {
|
|
198447
|
+
state: "ambiguous",
|
|
198448
|
+
code: "ambiguous_write",
|
|
198449
|
+
message: "the prior formal review marker is not visible yet; automatic mutation retry is blocked"
|
|
198450
|
+
};
|
|
198451
|
+
} catch {
|
|
198452
|
+
return {
|
|
198453
|
+
state: "ambiguous",
|
|
198454
|
+
code: "ambiguous_write",
|
|
198455
|
+
message: "cannot reconcile the prior formal review attempt; automatic mutation retry is blocked"
|
|
198456
|
+
};
|
|
198457
|
+
}
|
|
198458
|
+
}
|
|
198459
|
+
submitted(review, target, input) {
|
|
198460
|
+
if (review.id === void 0 || review.id === null) return {
|
|
198461
|
+
state: "ambiguous",
|
|
198462
|
+
code: "ambiguous_write",
|
|
198463
|
+
message: "GitHub created/recovered a review without an id"
|
|
198464
|
+
};
|
|
198465
|
+
if (!review.commit_id) return {
|
|
198466
|
+
state: "ambiguous",
|
|
198467
|
+
code: "ambiguous_write",
|
|
198468
|
+
message: "GitHub created/recovered a review without its commit id"
|
|
198469
|
+
};
|
|
198470
|
+
const commitId = review.commit_id;
|
|
198471
|
+
if (commitId !== target.expectedHeadSha) return {
|
|
198472
|
+
state: "ambiguous",
|
|
198473
|
+
code: "ambiguous_write",
|
|
198474
|
+
message: "recovered review is anchored to an unexpected commit"
|
|
198475
|
+
};
|
|
198476
|
+
return {
|
|
198477
|
+
state: "submitted",
|
|
198478
|
+
reviewId: String(review.id),
|
|
198479
|
+
event: input.event,
|
|
198480
|
+
verdict: input.verdict,
|
|
198481
|
+
commitId
|
|
198482
|
+
};
|
|
198483
|
+
}
|
|
198484
|
+
async findByMarker(target, marker) {
|
|
198485
|
+
for (let page = 1; page <= MAX_REVIEW_PAGES; page += 1) {
|
|
198486
|
+
const rows = await this.request(`/repos/${target.repoFullName}/pulls/${target.pullNumber}/reviews?per_page=${REVIEWS_PER_PAGE}&page=${page}`, target.token, "GET");
|
|
198487
|
+
const hit = rows.find((review) => review.body?.includes(marker));
|
|
198488
|
+
if (hit) return hit;
|
|
198489
|
+
if (rows.length < REVIEWS_PER_PAGE) return void 0;
|
|
198490
|
+
}
|
|
198491
|
+
throw new Error("review marker reconciliation exceeded pagination bound");
|
|
198492
|
+
}
|
|
198493
|
+
async request(path, token, method, body) {
|
|
198494
|
+
let response;
|
|
198495
|
+
try {
|
|
198496
|
+
response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
198497
|
+
method,
|
|
198498
|
+
headers: {
|
|
198499
|
+
authorization: `Bearer ${token}`,
|
|
198500
|
+
accept: "application/vnd.github+json",
|
|
198501
|
+
"x-github-api-version": "2022-11-28",
|
|
198502
|
+
...body === void 0 ? {} : { "content-type": "application/json" }
|
|
198503
|
+
},
|
|
198504
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
198505
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
198506
|
+
});
|
|
198507
|
+
} catch (err) {
|
|
198508
|
+
throw new Error(`GitHub request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
198509
|
+
}
|
|
198510
|
+
const text = await response.text();
|
|
198511
|
+
if (!response.ok) {
|
|
198512
|
+
let detail = "";
|
|
198513
|
+
try {
|
|
198514
|
+
const parsed = safeJson(text);
|
|
198515
|
+
detail = parsed?.message ? `: ${parsed.message}` : "";
|
|
198516
|
+
} catch {}
|
|
198517
|
+
throw new GithubHttpError(response.status, `GitHub ${method} ${response.status}${detail}`);
|
|
198518
|
+
}
|
|
198519
|
+
return safeJson(text);
|
|
198520
|
+
}
|
|
198521
|
+
};
|
|
198522
|
+
//#endregion
|
|
198128
198523
|
//#region src/runtimes/runtime-prober.ts
|
|
198129
198524
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
198130
198525
|
const DEFAULT_CONCURRENCY = 3;
|
|
@@ -198358,7 +198753,7 @@ var RelayClient = class {
|
|
|
198358
198753
|
const frame = decoded.frame;
|
|
198359
198754
|
if (frame.corr && this.correlator.settle(frame)) return;
|
|
198360
198755
|
if (frame.type === "rd/msg") {
|
|
198361
|
-
this.handleMsg(frame.id, frame.payload);
|
|
198756
|
+
await this.handleMsg(frame.id, frame.payload);
|
|
198362
198757
|
return;
|
|
198363
198758
|
}
|
|
198364
198759
|
if (frame.type === "rd/agentmsg/fwd") {
|
|
@@ -198370,9 +198765,9 @@ var RelayClient = class {
|
|
|
198370
198765
|
/** An inbound rd/msg: dispatch it, reply `rd/ack`; only a webchat op streams
|
|
198371
198766
|
* `rd/chat` back over this socket. A shared-bot `im` replies out-of-band via the
|
|
198372
198767
|
* daemon's own Slack send, and a hook fire has no reply stream — both get a noop. */
|
|
198373
|
-
handleMsg(reqId, msg) {
|
|
198768
|
+
async handleMsg(reqId, msg) {
|
|
198374
198769
|
const chat = msg.source === "webchat" ? (event) => this.sendChat(msg.chatId, event) : () => void 0;
|
|
198375
|
-
const ack = this.deps.onRelayMsg(msg, chat);
|
|
198770
|
+
const ack = await this.deps.onRelayMsg(msg, chat);
|
|
198376
198771
|
this.transport?.send(JSON.stringify(buildRelayDaemonFrame("rd/ack", ack, { corr: reqId })));
|
|
198377
198772
|
}
|
|
198378
198773
|
/** A forwarded cross-daemon agent-call: terminal-verify + dispatch, reply the typed
|
|
@@ -199160,6 +199555,8 @@ function findAgentFileById(agentsDir, agentId) {
|
|
|
199160
199555
|
*/
|
|
199161
199556
|
function applySpecFields(raw, spec, opts) {
|
|
199162
199557
|
raw.name = spec.name;
|
|
199558
|
+
if (spec.displayName !== void 0) if (spec.displayName === null) delete raw.displayName;
|
|
199559
|
+
else raw.displayName = spec.displayName;
|
|
199163
199560
|
if (spec.description !== void 0) raw.description = spec.description;
|
|
199164
199561
|
if (spec.runtime !== void 0) raw.runtime = spec.runtime;
|
|
199165
199562
|
if (spec.reasoningEffort !== void 0) if (spec.reasoningEffort === null) delete raw.reasoningEffort;
|
|
@@ -215509,10 +215906,35 @@ function isBuiltinSystemTool(params) {
|
|
|
215509
215906
|
tc?.toolCallId
|
|
215510
215907
|
].some((id) => typeof id === "string" && [...BUILTIN_TOOL_FQNS].some((fqn) => id.includes(fqn)));
|
|
215511
215908
|
}
|
|
215909
|
+
function hookSnapshot(msg) {
|
|
215910
|
+
if (msg.configRevision === void 0 || msg.dispatchRevision === void 0 || msg.dispatchDaemonId === void 0 || msg.reviewPolicy === void 0 || msg.reportingMode === void 0 || msg.gateMode === void 0) return void 0;
|
|
215911
|
+
return {
|
|
215912
|
+
configRevision: msg.configRevision,
|
|
215913
|
+
dispatchRevision: msg.dispatchRevision,
|
|
215914
|
+
dispatchDaemonId: msg.dispatchDaemonId,
|
|
215915
|
+
reviewPolicy: msg.reviewPolicy,
|
|
215916
|
+
reportingMode: msg.reportingMode,
|
|
215917
|
+
gateMode: msg.gateMode
|
|
215918
|
+
};
|
|
215919
|
+
}
|
|
215920
|
+
function reviewPolicyAllows(policy, event) {
|
|
215921
|
+
if (policy === "full") return true;
|
|
215922
|
+
if (policy === "request_changes") return event === "COMMENT" || event === "REQUEST_CHANGES";
|
|
215923
|
+
if (policy === "comment") return event === "COMMENT";
|
|
215924
|
+
return false;
|
|
215925
|
+
}
|
|
215926
|
+
function reviewResultForWire(effect) {
|
|
215927
|
+
if (effect.state === "submitted") return effect;
|
|
215928
|
+
return {
|
|
215929
|
+
state: effect.state,
|
|
215930
|
+
code: effect.code
|
|
215931
|
+
};
|
|
215932
|
+
}
|
|
215512
215933
|
const MAX_QUEUED_PER_SESSION = 10;
|
|
215513
215934
|
const LOOP_GUARD_WINDOW_MS = 6e4;
|
|
215514
215935
|
const MAX_AUTOMATIC_TURNS_PER_WINDOW = 8;
|
|
215515
215936
|
const MAX_TOTAL_TURNS_PER_WINDOW = 60;
|
|
215937
|
+
const MAX_HOOK_REPORT_INFLIGHT = 100;
|
|
215516
215938
|
/** Thrown to a `dispatch()` caller when the per-session admission queue is at its depth
|
|
215517
215939
|
* cap (§4.4 backpressure): the message is fast-failed, not buffered. Carries a stable
|
|
215518
215940
|
* `reason` so an agent-call source (P1/P2) can surface a typed `queue_full`. */
|
|
@@ -215608,6 +216030,22 @@ function usesLoopGuard(msg) {
|
|
|
215608
216030
|
function isMalformedPlatformTurn(msg) {
|
|
215609
216031
|
return msg.platform === "slack" && msg.source === "user" && !msg.sender.isBot && msg.sender.id === "unknown" && msg.text.trim() === "" && (msg.attachments?.length ?? 0) === 0;
|
|
215610
216032
|
}
|
|
216033
|
+
function authorizedReviewTargetMatches(active, attemptId, authorized) {
|
|
216034
|
+
return authorized.attemptId === attemptId && authorized.repoId === active.repoId && authorized.repoFullName.toLowerCase() === active.repoFullName.toLowerCase() && authorized.pullNumber === active.pullNumber && authorized.expectedHeadSha === active.expectedHeadSha && authorized.expectedBaseSha === active.expectedBaseSha;
|
|
216035
|
+
}
|
|
216036
|
+
function authorizedReviewTarget(active, attemptId, authorized, recovering = false) {
|
|
216037
|
+
return {
|
|
216038
|
+
token: authorized.token,
|
|
216039
|
+
repoFullName: authorized.repoFullName,
|
|
216040
|
+
pullNumber: authorized.pullNumber,
|
|
216041
|
+
expectedHeadSha: authorized.expectedHeadSha,
|
|
216042
|
+
expectedBaseSha: authorized.expectedBaseSha,
|
|
216043
|
+
hookId: active.hook.hookId,
|
|
216044
|
+
deliveryKey: active.hook.deliveryKey,
|
|
216045
|
+
attemptId,
|
|
216046
|
+
...recovering ? { recovering: true } : {}
|
|
216047
|
+
};
|
|
216048
|
+
}
|
|
215611
216049
|
function isSlackStatusBarText(text) {
|
|
215612
216050
|
return text.startsWith(":bar_chart:") || text.startsWith("📊");
|
|
215613
216051
|
}
|
|
@@ -215692,6 +216130,10 @@ var Daemon = class Daemon {
|
|
|
215692
216130
|
botUserIds = {};
|
|
215693
216131
|
cpRouting;
|
|
215694
216132
|
cpCollab = new CpCollabRoutes();
|
|
216133
|
+
/** One active PR hook turn per logical session key. Long-lived ACP sessions
|
|
216134
|
+
* may span deliveries, so authorization must never live in SessionContext. */
|
|
216135
|
+
activeGithubTurnMeta = /* @__PURE__ */ new Map();
|
|
216136
|
+
githubReviewClient = new GithubReviewClient();
|
|
215695
216137
|
clock;
|
|
215696
216138
|
requestExit;
|
|
215697
216139
|
draining = false;
|
|
@@ -215715,6 +216157,9 @@ var Daemon = class Daemon {
|
|
|
215715
216157
|
cancelTimers = /* @__PURE__ */ new Map();
|
|
215716
216158
|
coldCancelTimers = /* @__PURE__ */ new Map();
|
|
215717
216159
|
slackRetryTimers = /* @__PURE__ */ new Map();
|
|
216160
|
+
hookReportRetryTimer;
|
|
216161
|
+
hookReportInflight = /* @__PURE__ */ new Set();
|
|
216162
|
+
hookReportConnectionId = randomUUID();
|
|
215718
216163
|
slackRetryRuns = /* @__PURE__ */ new Map();
|
|
215719
216164
|
constructor(opts = {}) {
|
|
215720
216165
|
this.opts = opts;
|
|
@@ -215844,6 +216289,7 @@ var Daemon = class Daemon {
|
|
|
215844
216289
|
startOrchestration: (req) => this.startOrchestration(req),
|
|
215845
216290
|
getOrchestration: (req) => Promise.resolve(this.getOrchestrationForOwner(req)),
|
|
215846
216291
|
cancelOrchestration: (req) => Promise.resolve(this.cancelOrchestrationForOwner(req)),
|
|
216292
|
+
submitGithubReview: (req) => this.submitGithubReview(req),
|
|
215847
216293
|
memory: this.memory,
|
|
215848
216294
|
recordOutbound: (ctx, channel, thread, text, ts) => this.store.appendTranscript({
|
|
215849
216295
|
channel,
|
|
@@ -215865,6 +216311,7 @@ var Daemon = class Daemon {
|
|
|
215865
216311
|
mcpServersFor: ({ agent, platform, channel, thread, integrationId, isDm }) => {
|
|
215866
216312
|
const servers = [];
|
|
215867
216313
|
let tools = toolsForIntegrations(agent.integrations);
|
|
216314
|
+
tools = [...tools, ...GITHUB_REVIEW_TOOLS];
|
|
215868
216315
|
if (memoryKindOf(agent) === "native") tools = tools.filter((t) => !MEMORY_TOOL_NAMES.has(t.name));
|
|
215869
216316
|
if (tools.length > 0) {
|
|
215870
216317
|
const token = this.mcp.register({
|
|
@@ -216872,6 +217319,10 @@ var Daemon = class Daemon {
|
|
|
216872
217319
|
this.log.debug(`webchat cancel: no in-flight turn for conversation ${conversationId}`);
|
|
216873
217320
|
}
|
|
216874
217321
|
relayMsgAcks = /* @__PURE__ */ new Map();
|
|
217322
|
+
/** Hook admission crosses an async anchor + durable-inbox barrier. Coalesce a
|
|
217323
|
+
* retransmit that arrives before that barrier settles instead of dispatching
|
|
217324
|
+
* the same delivery twice. */
|
|
217325
|
+
pendingRelayMsgAcks = /* @__PURE__ */ new Map();
|
|
216875
217326
|
agentCallDeliveries = /* @__PURE__ */ new Map();
|
|
216876
217327
|
orchestrationDeadlines = /* @__PURE__ */ new Map();
|
|
216877
217328
|
/**
|
|
@@ -216893,7 +217344,26 @@ var Daemon = class Daemon {
|
|
|
216893
217344
|
this.log.debug(`relay: duplicate rd/msg ${dedupKey} — replaying ack (no re-dispatch)`);
|
|
216894
217345
|
return prior;
|
|
216895
217346
|
}
|
|
216896
|
-
const
|
|
217347
|
+
const pending = this.pendingRelayMsgAcks.get(dedupKey);
|
|
217348
|
+
if (pending) return pending;
|
|
217349
|
+
if (msg.source === "hook") {
|
|
217350
|
+
const task = this.dispatchRelayHook(msg).catch((err) => {
|
|
217351
|
+
this.log.error(`hook admission failed for ${dedupKey}: ${formatErr(err)}`);
|
|
217352
|
+
return {
|
|
217353
|
+
msgId: msg.msgId,
|
|
217354
|
+
accepted: false,
|
|
217355
|
+
reason: "durability"
|
|
217356
|
+
};
|
|
217357
|
+
}).then((ack) => {
|
|
217358
|
+
this.pendingRelayMsgAcks.delete(dedupKey);
|
|
217359
|
+
if (this.relayMsgAcks.size >= 2e3) this.relayMsgAcks.clear();
|
|
217360
|
+
this.relayMsgAcks.set(dedupKey, ack);
|
|
217361
|
+
return ack;
|
|
217362
|
+
});
|
|
217363
|
+
this.pendingRelayMsgAcks.set(dedupKey, task);
|
|
217364
|
+
return task;
|
|
217365
|
+
}
|
|
217366
|
+
const ack = msg.source === "webchat" ? this.dispatchRelayOp(msg, chat) : this.handleRelayIm(msg);
|
|
216897
217367
|
if (this.relayMsgAcks.size >= 2e3) this.relayMsgAcks.clear();
|
|
216898
217368
|
this.relayMsgAcks.set(dedupKey, ack);
|
|
216899
217369
|
return ack;
|
|
@@ -217395,9 +217865,10 @@ var Daemon = class Daemon {
|
|
|
217395
217865
|
* Ack-verdict gate for one hook fire (`rd/msg` hook member) — the mirror of
|
|
217396
217866
|
* {@link dispatchWebchatTurn}'s synchronous gates: the relay's rc/run-report
|
|
217397
217867
|
* needs a REASONED rejection now, not a silently dropped fire-and-forget
|
|
217398
|
-
* dispatch. Accepted
|
|
217868
|
+
* dispatch. Accepted is returned only after {@link onHookFire} has crossed
|
|
217869
|
+
* the durable-inbox admission barrier; the model turn itself remains async.
|
|
217399
217870
|
*/
|
|
217400
|
-
dispatchRelayHook(msg) {
|
|
217871
|
+
async dispatchRelayHook(msg) {
|
|
217401
217872
|
if (!this.agents.has(msg.agentId)) {
|
|
217402
217873
|
this.log.warn(`hook: no agent "${msg.agentId}" on this daemon — rejecting fire ${msg.msgId}`);
|
|
217403
217874
|
return {
|
|
@@ -217430,10 +217901,11 @@ var Daemon = class Daemon {
|
|
|
217430
217901
|
reason: "draining"
|
|
217431
217902
|
};
|
|
217432
217903
|
}
|
|
217433
|
-
|
|
217904
|
+
const admission = await this.onHookFire(msg);
|
|
217434
217905
|
return {
|
|
217435
217906
|
msgId: msg.msgId,
|
|
217436
|
-
accepted:
|
|
217907
|
+
accepted: admission.accepted,
|
|
217908
|
+
...admission.reason ? { reason: admission.reason } : {}
|
|
217437
217909
|
};
|
|
217438
217910
|
}
|
|
217439
217911
|
/**
|
|
@@ -217446,25 +217918,36 @@ var Daemon = class Daemon {
|
|
|
217446
217918
|
* without one the fire runs headless.
|
|
217447
217919
|
*/
|
|
217448
217920
|
async onHookFire(msg) {
|
|
217449
|
-
const firedAt = this.clock.now();
|
|
217450
|
-
const report = (status, extra = {}) => {
|
|
217451
|
-
this.cpClient?.emitHookReport({
|
|
217452
|
-
hookId: msg.hookId,
|
|
217453
|
-
agentId: msg.agentId,
|
|
217454
|
-
deliveryKey: msg.deliveryKey,
|
|
217455
|
-
status,
|
|
217456
|
-
durationMs: Math.max(0, this.clock.now() - firedAt),
|
|
217457
|
-
...extra
|
|
217458
|
-
});
|
|
217459
|
-
};
|
|
217460
217921
|
const nmsg = buildHookMessage(msg, randomUUID());
|
|
217922
|
+
if (this.store.hasInbox(nmsg.msgId)) {
|
|
217923
|
+
this.log.debug(`hook: durable duplicate ${nmsg.msgId} — replaying accepted admission`);
|
|
217924
|
+
return { accepted: true };
|
|
217925
|
+
}
|
|
217926
|
+
const snapshot = hookSnapshot(msg);
|
|
217927
|
+
const hookContext = {
|
|
217928
|
+
hookId: msg.hookId,
|
|
217929
|
+
agentId: msg.agentId,
|
|
217930
|
+
deliveryKey: msg.deliveryKey,
|
|
217931
|
+
firedAt: msg.firedAt,
|
|
217932
|
+
...msg.event ? { event: msg.event } : {},
|
|
217933
|
+
...snapshot ? { snapshot } : {},
|
|
217934
|
+
...msg.github ? { github: msg.github } : {}
|
|
217935
|
+
};
|
|
217461
217936
|
const c = msg.context;
|
|
217462
217937
|
if (c?.source === "github" && c.action === "deleted" && !msg.target) {
|
|
217463
217938
|
const key = sessionKey(nmsg.platform, nmsg.channel, nmsg.thread ?? nmsg.msgId, msg.agentId);
|
|
217464
217939
|
if (!this.store.getSession(key)?.acpSessionId) {
|
|
217465
217940
|
this.log.info(`hook: skipping github ${c.event ?? "event"}:deleted fire ${msg.msgId} — no session for ${key}`);
|
|
217466
|
-
|
|
217467
|
-
|
|
217941
|
+
const entry = {
|
|
217942
|
+
agentId: msg.agentId,
|
|
217943
|
+
msg: nmsg,
|
|
217944
|
+
initAbort: new AbortController(),
|
|
217945
|
+
hookContext,
|
|
217946
|
+
resolve: () => {},
|
|
217947
|
+
reject: () => {}
|
|
217948
|
+
};
|
|
217949
|
+
if (this.persistInbox(entry, key, { required: true }) !== "existing") this.emitHookCompletion(hookContext, "success", { reason: "deleted: no existing session" }, entry);
|
|
217950
|
+
return { accepted: true };
|
|
217468
217951
|
}
|
|
217469
217952
|
}
|
|
217470
217953
|
const githubReply = c?.source === "github" && c.repo && c.number !== void 0 ? {
|
|
@@ -217472,14 +217955,223 @@ var Daemon = class Daemon {
|
|
|
217472
217955
|
repo: c.repo,
|
|
217473
217956
|
number: c.number
|
|
217474
217957
|
} : void 0;
|
|
217958
|
+
if (githubReply) hookContext.githubReply = githubReply;
|
|
217959
|
+
const anchored = await this.anchorTrigger(msg.agentId, nmsg, msg.target, hookAnchorText(msg), `hook "${msg.hookId}"`);
|
|
217960
|
+
if (!anchored) return {
|
|
217961
|
+
accepted: false,
|
|
217962
|
+
reason: "dropped"
|
|
217963
|
+
};
|
|
217964
|
+
let admission;
|
|
217965
|
+
this.dispatch(msg.agentId, anchored, msg.target?.integrationId, void 0, void 0, {
|
|
217966
|
+
requireDurable: true,
|
|
217967
|
+
onAdmission: (result) => {
|
|
217968
|
+
admission = result;
|
|
217969
|
+
}
|
|
217970
|
+
}, githubReply, hookContext).catch((err) => this.log.error(`hook turn failed for agent "${msg.agentId}": ${formatErr(err)}`));
|
|
217971
|
+
if (!admission) throw new Error("hook admission barrier did not settle synchronously");
|
|
217972
|
+
if (!admission.accepted) return {
|
|
217973
|
+
accepted: false,
|
|
217974
|
+
reason: admission.reason ?? "durability"
|
|
217975
|
+
};
|
|
217976
|
+
return { accepted: true };
|
|
217977
|
+
}
|
|
217978
|
+
/** Resolve a PR revision if the webhook omitted it (notably
|
|
217979
|
+
* issue_comment), cross the CP hook/start barrier, then return the active
|
|
217980
|
+
* review authority. Failure only disables structured effects; the ordinary
|
|
217981
|
+
* agent turn and final poster continue. */
|
|
217982
|
+
async prepareGithubTurn(entry) {
|
|
217983
|
+
const hook = entry.hookContext;
|
|
217984
|
+
const snapshot = hook?.snapshot;
|
|
217985
|
+
const github = hook?.github;
|
|
217986
|
+
if (!hook || !snapshot || !github || github.subjectKind !== "pull_request" || github.pullNumber === void 0 || snapshot.reviewPolicy === "off" && snapshot.reportingMode === "off" || snapshot.gateMode !== "informational" || snapshot.reportingMode === "status") return void 0;
|
|
217987
|
+
if (this.cfg.daemonId && snapshot.dispatchDaemonId !== this.cfg.daemonId) {
|
|
217988
|
+
this.log.warn(`github review: stale dispatch daemon for ${hook.hookId}:${hook.deliveryKey}`);
|
|
217989
|
+
return;
|
|
217990
|
+
}
|
|
217991
|
+
if (!github.headSha || !github.baseSha) try {
|
|
217992
|
+
const postToken = await this.gitCreds.getPostToken(hook.agentId, github.repoFullName, hook.hookId);
|
|
217993
|
+
const revision = await this.githubReviewClient.getPull(postToken.token, github.repoFullName, github.pullNumber);
|
|
217994
|
+
hook.github = {
|
|
217995
|
+
...github,
|
|
217996
|
+
headSha: revision.headSha,
|
|
217997
|
+
baseSha: revision.baseSha,
|
|
217998
|
+
reportSha: revision.headSha,
|
|
217999
|
+
isDraft: revision.draft
|
|
218000
|
+
};
|
|
218001
|
+
this.persistHookState(entry, void 0, true);
|
|
218002
|
+
} catch (err) {
|
|
218003
|
+
this.log.warn(`github review: unable to resolve PR revision (${formatErr(err)})`);
|
|
218004
|
+
return;
|
|
218005
|
+
}
|
|
218006
|
+
const trusted = hook.github;
|
|
218007
|
+
if (!trusted.headSha || !trusted.baseSha || trusted.pullNumber === void 0) return void 0;
|
|
218008
|
+
const client = this.cpClient;
|
|
218009
|
+
if (!client) return void 0;
|
|
218010
|
+
const payload = {
|
|
218011
|
+
hookId: hook.hookId,
|
|
218012
|
+
agentId: hook.agentId,
|
|
218013
|
+
deliveryKey: hook.deliveryKey,
|
|
218014
|
+
...hook.event ? { event: hook.event } : {},
|
|
218015
|
+
github: {
|
|
218016
|
+
...trusted,
|
|
218017
|
+
reportSha: trusted.reportSha ?? trusted.headSha
|
|
218018
|
+
},
|
|
218019
|
+
...snapshot
|
|
218020
|
+
};
|
|
218021
|
+
let started = false;
|
|
218022
|
+
for (let attempt = 0; attempt < 3 && !started; attempt += 1) try {
|
|
218023
|
+
await client.startHook(payload);
|
|
218024
|
+
started = true;
|
|
218025
|
+
} catch (err) {
|
|
218026
|
+
if (attempt === 2) {
|
|
218027
|
+
this.log.warn(`github review: hook/start rejected (${formatErr(err)})`);
|
|
218028
|
+
return;
|
|
218029
|
+
}
|
|
218030
|
+
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
|
218031
|
+
}
|
|
218032
|
+
if (snapshot.reviewPolicy === "off") return void 0;
|
|
218033
|
+
const recoverableAttempt = hook.reviewAttemptId !== void 0 && hook.reviewRequestedEvent !== void 0 && hook.reviewRequestedVerdict !== void 0 && (hook.reviewResult === void 0 || hook.reviewResult.state === "ambiguous");
|
|
218034
|
+
const active = {
|
|
218035
|
+
entry,
|
|
218036
|
+
hook,
|
|
218037
|
+
snapshot,
|
|
218038
|
+
repoId: trusted.repoId,
|
|
218039
|
+
repoFullName: trusted.repoFullName,
|
|
218040
|
+
pullNumber: trusted.pullNumber,
|
|
218041
|
+
expectedHeadSha: trusted.headSha,
|
|
218042
|
+
expectedBaseSha: trusted.baseSha,
|
|
218043
|
+
reportSha: trusted.reportSha ?? trusted.headSha,
|
|
218044
|
+
reviewState: hook.reviewAttemptId === void 0 || recoverableAttempt ? "idle" : "done"
|
|
218045
|
+
};
|
|
218046
|
+
if (recoverableAttempt) await this.reconcileGithubReviewAttempt(active);
|
|
218047
|
+
return active;
|
|
218048
|
+
}
|
|
218049
|
+
/** Store the immediate and terminal-report copies together so they cannot
|
|
218050
|
+
* drift across submit and restart-recovery paths. */
|
|
218051
|
+
persistGithubReviewEffect(active, attemptId, effect, required = false) {
|
|
218052
|
+
const result = reviewResultForWire(effect);
|
|
218053
|
+
active.hook.reviewResult = result;
|
|
218054
|
+
active.hook.reviewReportAttemptId = attemptId;
|
|
218055
|
+
active.hook.reviewReportResult = result;
|
|
218056
|
+
this.persistHookState(active.entry, void 0, required);
|
|
218057
|
+
return result;
|
|
218058
|
+
}
|
|
218059
|
+
/** A daemon restart may replay an attempt after an ambiguous POST/list race.
|
|
218060
|
+
* Before prompting the model, perform a GET-only marker reconciliation. A
|
|
218061
|
+
* still-missing marker remains blocked; only an explicit tool invocation can
|
|
218062
|
+
* cross the full reauthorization + revision fence and retry the mutation. */
|
|
218063
|
+
async reconcileGithubReviewAttempt(active) {
|
|
218064
|
+
const cp = this.cpClient;
|
|
218065
|
+
const attemptId = active.hook.reviewAttemptId;
|
|
218066
|
+
const requestedEvent = active.hook.reviewRequestedEvent;
|
|
218067
|
+
const requestedVerdict = active.hook.reviewRequestedVerdict;
|
|
218068
|
+
if (!cp || !attemptId || !requestedEvent || !requestedVerdict) return;
|
|
218069
|
+
try {
|
|
218070
|
+
const authorized = await cp.authorizeGithubReview({
|
|
218071
|
+
hookId: active.hook.hookId,
|
|
218072
|
+
deliveryKey: active.hook.deliveryKey,
|
|
218073
|
+
attemptId,
|
|
218074
|
+
requestedEvent,
|
|
218075
|
+
requestedVerdict,
|
|
218076
|
+
snapshot: active.snapshot
|
|
218077
|
+
});
|
|
218078
|
+
if (!authorizedReviewTargetMatches(active, attemptId, authorized)) {
|
|
218079
|
+
this.log.warn("github review: recovery authorization returned a mismatched target");
|
|
218080
|
+
return;
|
|
218081
|
+
}
|
|
218082
|
+
const effect = await this.githubReviewClient.reconcile(authorizedReviewTarget(active, attemptId, authorized, true), requestedEvent, requestedVerdict);
|
|
218083
|
+
if (effect.state !== "submitted") return;
|
|
218084
|
+
const result = this.persistGithubReviewEffect(active, attemptId, effect, true);
|
|
218085
|
+
await cp.reportGithubReviewResult({
|
|
218086
|
+
hookId: active.hook.hookId,
|
|
218087
|
+
deliveryKey: active.hook.deliveryKey,
|
|
218088
|
+
attemptId,
|
|
218089
|
+
snapshot: active.snapshot,
|
|
218090
|
+
result
|
|
218091
|
+
});
|
|
218092
|
+
active.reviewState = "done";
|
|
218093
|
+
} catch (err) {
|
|
218094
|
+
this.log.warn(`github review: replay reconciliation deferred (${formatErr(err)})`);
|
|
218095
|
+
}
|
|
218096
|
+
}
|
|
218097
|
+
async submitGithubReview(req) {
|
|
218098
|
+
const key = sessionKey(req.platform, req.channel, req.thread, req.agentId);
|
|
218099
|
+
const active = this.activeGithubTurnMeta.get(key);
|
|
218100
|
+
if (!active || active.hook.agentId !== req.agentId) throw new Error("formal GitHub review is only available during the active PR hook turn");
|
|
218101
|
+
if (!reviewPolicyAllows(active.snapshot.reviewPolicy, req.event)) throw new Error(`${req.event} exceeds this hook's ${active.snapshot.reviewPolicy} review policy`);
|
|
218102
|
+
if (active.reviewState !== "idle") throw new Error("this PR hook turn already has a formal review attempt");
|
|
218103
|
+
active.reviewState = "submitting";
|
|
218104
|
+
const recovering = active.hook.reviewAttemptId !== void 0;
|
|
218105
|
+
if (recovering) {
|
|
218106
|
+
if (active.hook.reviewRequestedEvent === void 0 || active.hook.reviewRequestedVerdict === void 0 || active.hook.reviewRequestedEvent !== req.event || active.hook.reviewRequestedVerdict !== req.verdict) {
|
|
218107
|
+
active.reviewState = "idle";
|
|
218108
|
+
throw new Error("a recovered formal-review attempt must keep its original event and verdict");
|
|
218109
|
+
}
|
|
218110
|
+
} else {
|
|
218111
|
+
active.hook.reviewRequestedEvent = req.event;
|
|
218112
|
+
active.hook.reviewRequestedVerdict = req.verdict;
|
|
218113
|
+
}
|
|
218114
|
+
const attemptId = active.hook.reviewAttemptId ?? randomUUID();
|
|
218115
|
+
active.hook.reviewAttemptId = attemptId;
|
|
218116
|
+
const entry = active.entry;
|
|
217475
218117
|
try {
|
|
217476
|
-
|
|
217477
|
-
if (sessionId !== null) report("success", { sessionId });
|
|
217478
|
-
else report("failed", { reason: "dropped" });
|
|
218118
|
+
this.persistHookState(entry, void 0, true);
|
|
217479
218119
|
} catch (err) {
|
|
217480
|
-
|
|
218120
|
+
if (!recovering) {
|
|
218121
|
+
delete active.hook.reviewAttemptId;
|
|
218122
|
+
delete active.hook.reviewRequestedEvent;
|
|
218123
|
+
delete active.hook.reviewRequestedVerdict;
|
|
218124
|
+
}
|
|
218125
|
+
active.reviewState = "idle";
|
|
218126
|
+
throw new Error(`formal review durability barrier failed: ${formatErr(err)}`);
|
|
218127
|
+
}
|
|
218128
|
+
const cp = this.cpClient;
|
|
218129
|
+
if (!cp) {
|
|
218130
|
+
active.reviewState = "done";
|
|
218131
|
+
throw new Error("control plane is not connected; formal review denied");
|
|
218132
|
+
}
|
|
218133
|
+
let authorized;
|
|
218134
|
+
try {
|
|
218135
|
+
authorized = await cp.authorizeGithubReview({
|
|
218136
|
+
hookId: active.hook.hookId,
|
|
218137
|
+
deliveryKey: active.hook.deliveryKey,
|
|
218138
|
+
attemptId,
|
|
218139
|
+
requestedEvent: req.event,
|
|
218140
|
+
requestedVerdict: req.verdict,
|
|
218141
|
+
snapshot: active.snapshot
|
|
218142
|
+
});
|
|
218143
|
+
} catch (err) {
|
|
218144
|
+
active.reviewState = "done";
|
|
217481
218145
|
throw err;
|
|
217482
218146
|
}
|
|
218147
|
+
if (!authorizedReviewTargetMatches(active, attemptId, authorized)) {
|
|
218148
|
+
active.reviewState = "done";
|
|
218149
|
+
throw new Error("control plane returned a mismatched formal-review target");
|
|
218150
|
+
}
|
|
218151
|
+
const effect = await this.githubReviewClient.submit(authorizedReviewTarget(active, attemptId, authorized, recovering), req);
|
|
218152
|
+
const result = this.persistGithubReviewEffect(active, attemptId, effect);
|
|
218153
|
+
try {
|
|
218154
|
+
await cp.reportGithubReviewResult({
|
|
218155
|
+
hookId: active.hook.hookId,
|
|
218156
|
+
deliveryKey: active.hook.deliveryKey,
|
|
218157
|
+
attemptId,
|
|
218158
|
+
snapshot: active.snapshot,
|
|
218159
|
+
result
|
|
218160
|
+
});
|
|
218161
|
+
if (effect.state === "not_submitted") {
|
|
218162
|
+
delete active.hook.reviewAttemptId;
|
|
218163
|
+
delete active.hook.reviewRequestedEvent;
|
|
218164
|
+
delete active.hook.reviewRequestedVerdict;
|
|
218165
|
+
delete active.hook.reviewResult;
|
|
218166
|
+
active.reviewState = "idle";
|
|
218167
|
+
this.persistHookState(entry);
|
|
218168
|
+
} else if (effect.state === "ambiguous") active.reviewState = "idle";
|
|
218169
|
+
else active.reviewState = "done";
|
|
218170
|
+
} catch (err) {
|
|
218171
|
+
active.reviewState = effect.state === "ambiguous" ? "idle" : "done";
|
|
218172
|
+
this.log.warn(`github review: immediate result report failed (${formatErr(err)})`);
|
|
218173
|
+
}
|
|
218174
|
+
return effect;
|
|
217483
218175
|
}
|
|
217484
218176
|
/** The op-switch behind {@link handleRelayMsg} (dedup handled by the caller). */
|
|
217485
218177
|
dispatchRelayOp(msg, chat) {
|
|
@@ -218073,26 +218765,142 @@ var Daemon = class Daemon {
|
|
|
218073
218765
|
* durable row would be un-replayable. Sets `entry.inboxId` so every terminal path can delete
|
|
218074
218766
|
* the row. The row id is the message's stable deliveryId/msgId (§6.3) — idempotent re-append.
|
|
218075
218767
|
*/
|
|
218076
|
-
persistInbox(entry, key) {
|
|
218077
|
-
if (entry.webchat) return;
|
|
218768
|
+
persistInbox(entry, key, options = {}) {
|
|
218769
|
+
if (entry.webchat) return "skipped";
|
|
218078
218770
|
const id = entry.callMeta?.deliveryId ?? entry.msg.msgId;
|
|
218079
218771
|
try {
|
|
218080
|
-
this.store.appendInbox({
|
|
218772
|
+
const inserted = this.store.appendInbox({
|
|
218081
218773
|
id,
|
|
218082
218774
|
sessionKey: key,
|
|
218083
218775
|
agentId: entry.agentId,
|
|
218084
218776
|
msg: JSON.stringify(entry.msg),
|
|
218085
218777
|
integrationId: entry.integrationId ?? null,
|
|
218086
218778
|
callMeta: entry.callMeta ? JSON.stringify(entry.callMeta) : null,
|
|
218779
|
+
hookContext: entry.hookContext ? JSON.stringify(entry.hookContext) : null,
|
|
218780
|
+
posterPublishState: entry.posterPublishState ?? null,
|
|
218087
218781
|
isQueueCmd: entry.isQueueCmd ? 1 : null,
|
|
218088
218782
|
loopGuardCounted: 1,
|
|
218089
218783
|
enqueuedAt: monotonicTs()
|
|
218090
218784
|
});
|
|
218785
|
+
if (!inserted && !options.adoptExisting) return "existing";
|
|
218091
218786
|
entry.inboxId = id;
|
|
218092
218787
|
this.liveInboxIds.add(id);
|
|
218788
|
+
return inserted ? "inserted" : "adopted";
|
|
218093
218789
|
} catch (err) {
|
|
218790
|
+
if (options.required) throw err;
|
|
218094
218791
|
this.log.warn(`durable inbox: append failed for ${id}: ${err.message}`);
|
|
218792
|
+
return "failed";
|
|
218793
|
+
}
|
|
218794
|
+
}
|
|
218795
|
+
persistHookState(entry, posterPublishState, required = false) {
|
|
218796
|
+
if (!entry.inboxId || !entry.hookContext) {
|
|
218797
|
+
if (required) throw new Error("hook state has no durable inbox row");
|
|
218798
|
+
return;
|
|
218799
|
+
}
|
|
218800
|
+
if (posterPublishState) entry.posterPublishState = posterPublishState;
|
|
218801
|
+
try {
|
|
218802
|
+
if (!this.store.updateInboxHookState(entry.inboxId, JSON.stringify(entry.hookContext), posterPublishState)) throw new Error("durable inbox row is missing");
|
|
218803
|
+
} catch (err) {
|
|
218804
|
+
if (required) throw err;
|
|
218805
|
+
this.log.warn(`durable inbox: hook state update failed for ${entry.inboxId}: ${err.message}`);
|
|
218806
|
+
}
|
|
218807
|
+
}
|
|
218808
|
+
emitHookCompletion(hook, status, extra = {}, owner) {
|
|
218809
|
+
if (owner?.hookTerminalReceipt) return;
|
|
218810
|
+
const start = Date.parse(hook.turnStartedAt ?? hook.firedAt);
|
|
218811
|
+
const reviewAttemptId = hook.reviewReportAttemptId ?? hook.reviewAttemptId;
|
|
218812
|
+
const reviewResult = hook.reviewReportResult ?? hook.reviewResult;
|
|
218813
|
+
const report = {
|
|
218814
|
+
hookId: hook.hookId,
|
|
218815
|
+
agentId: hook.agentId,
|
|
218816
|
+
deliveryKey: hook.deliveryKey,
|
|
218817
|
+
...hook.snapshot ?? {},
|
|
218818
|
+
...hook.event ? { event: hook.event } : {},
|
|
218819
|
+
...hook.github ? { github: hook.github } : {},
|
|
218820
|
+
status,
|
|
218821
|
+
durationMs: Number.isFinite(start) ? Math.max(0, this.clock.now() - start) : 0,
|
|
218822
|
+
...extra,
|
|
218823
|
+
...reviewAttemptId && reviewResult ? {
|
|
218824
|
+
reviewAttemptId,
|
|
218825
|
+
reviewResult
|
|
218826
|
+
} : {}
|
|
218827
|
+
};
|
|
218828
|
+
let reportInboxId;
|
|
218829
|
+
if (owner?.inboxId) try {
|
|
218830
|
+
const completed = this.store.completeHookInbox(owner.inboxId, JSON.stringify(report), this.clock.now());
|
|
218831
|
+
if (completed === "already-terminal") {
|
|
218832
|
+
owner.hookTerminalReceipt = true;
|
|
218833
|
+
this.liveInboxIds.delete(owner.inboxId);
|
|
218834
|
+
return;
|
|
218835
|
+
}
|
|
218836
|
+
if (completed === "missing") {
|
|
218837
|
+
this.log.warn(`durable inbox: hook terminal receipt owner is missing for ${owner.inboxId}`);
|
|
218838
|
+
this.liveInboxIds.delete(owner.inboxId);
|
|
218839
|
+
} else {
|
|
218840
|
+
owner.hookTerminalReceipt = true;
|
|
218841
|
+
this.liveInboxIds.delete(owner.inboxId);
|
|
218842
|
+
reportInboxId = owner.inboxId;
|
|
218843
|
+
}
|
|
218844
|
+
} catch (err) {
|
|
218845
|
+
this.log.warn(`durable inbox: hook terminal receipt failed for ${owner.inboxId}: ${formatErr(err)}`);
|
|
218095
218846
|
}
|
|
218847
|
+
this.sendHookReport(report, reportInboxId);
|
|
218848
|
+
}
|
|
218849
|
+
/** Send one durable terminal report and release only its outbox payload after
|
|
218850
|
+
* the CP's correlated persistence/projection ACK. The stable id receipt stays
|
|
218851
|
+
* bounded locally to absorb later relay redelivery. */
|
|
218852
|
+
sendHookReport(report, inboxId) {
|
|
218853
|
+
if (!this.cpClient) return;
|
|
218854
|
+
if (inboxId && this.hookReportInflight.has(inboxId)) return;
|
|
218855
|
+
if (inboxId && this.hookReportInflight.size >= MAX_HOOK_REPORT_INFLIGHT) {
|
|
218856
|
+
this.scheduleHookReportRetry();
|
|
218857
|
+
return;
|
|
218858
|
+
}
|
|
218859
|
+
if (inboxId) this.hookReportInflight.add(inboxId);
|
|
218860
|
+
const connectionId = this.hookReportConnectionId;
|
|
218861
|
+
let refillDrainSlot = false;
|
|
218862
|
+
Promise.resolve(this.cpClient.emitHookReport(report)).then((result) => {
|
|
218863
|
+
if (!inboxId) return;
|
|
218864
|
+
if (result === "legacy-sent") {
|
|
218865
|
+
try {
|
|
218866
|
+
if (!this.store.markLegacyHookReportSent(inboxId, connectionId)) throw new Error("durable inbox row is missing");
|
|
218867
|
+
refillDrainSlot = true;
|
|
218868
|
+
} catch (err) {
|
|
218869
|
+
this.log.warn(`durable inbox: legacy report stamp failed for ${inboxId}: ${formatErr(err)}`);
|
|
218870
|
+
this.scheduleHookReportRetry();
|
|
218871
|
+
}
|
|
218872
|
+
return;
|
|
218873
|
+
}
|
|
218874
|
+
refillDrainSlot = true;
|
|
218875
|
+
try {
|
|
218876
|
+
this.store.acknowledgeHookInbox(inboxId);
|
|
218877
|
+
} catch (err) {
|
|
218878
|
+
this.log.warn(`durable inbox: hook report ACK cleanup failed for ${inboxId}: ${formatErr(err)}`);
|
|
218879
|
+
}
|
|
218880
|
+
}).catch((err) => {
|
|
218881
|
+
if (typeof err === "object" && err !== null && "retryable" in err && err.retryable === false && inboxId) {
|
|
218882
|
+
refillDrainSlot = true;
|
|
218883
|
+
try {
|
|
218884
|
+
this.store.acknowledgeHookInbox(inboxId);
|
|
218885
|
+
} catch (cleanupError) {
|
|
218886
|
+
this.log.warn(`durable inbox: hook report dead-letter failed for ${inboxId}: ${formatErr(cleanupError)}`);
|
|
218887
|
+
}
|
|
218888
|
+
this.log.warn(`hook report permanently rejected for ${inboxId}: ${formatErr(err)}`);
|
|
218889
|
+
return;
|
|
218890
|
+
}
|
|
218891
|
+
this.log.debug(`hook report retained for retry: ${formatErr(err)}`);
|
|
218892
|
+
this.scheduleHookReportRetry();
|
|
218893
|
+
}).finally(() => {
|
|
218894
|
+
if (inboxId) this.hookReportInflight.delete(inboxId);
|
|
218895
|
+
if (refillDrainSlot) this.scheduleHookReportRetry(250);
|
|
218896
|
+
});
|
|
218897
|
+
}
|
|
218898
|
+
scheduleHookReportRetry(delayMs = 5e3) {
|
|
218899
|
+
if (this.draining || this.hookReportRetryTimer !== void 0) return;
|
|
218900
|
+
this.hookReportRetryTimer = this.clock.setTimeout(() => {
|
|
218901
|
+
this.hookReportRetryTimer = void 0;
|
|
218902
|
+
this.replayHookTerminalReports();
|
|
218903
|
+
}, delayMs);
|
|
218096
218904
|
}
|
|
218097
218905
|
/** Remove an entry's durable inbox row once its turn reaches ANY terminal state (success,
|
|
218098
218906
|
* reject/fail-stop, cancel, gate-drop) so it is not replayed on the next startup. No-op
|
|
@@ -218101,6 +218909,7 @@ var Daemon = class Daemon {
|
|
|
218101
218909
|
removeInbox(entry) {
|
|
218102
218910
|
if (!entry.inboxId) return;
|
|
218103
218911
|
this.liveInboxIds.delete(entry.inboxId);
|
|
218912
|
+
if (entry.hookContext) return;
|
|
218104
218913
|
try {
|
|
218105
218914
|
this.store.removeInbox(entry.inboxId);
|
|
218106
218915
|
} catch (err) {
|
|
@@ -218128,6 +218937,7 @@ var Daemon = class Daemon {
|
|
|
218128
218937
|
let removed = 0;
|
|
218129
218938
|
try {
|
|
218130
218939
|
for (const row of this.store.listInboxBySessionKeyFifo()) {
|
|
218940
|
+
if (row.hookContext || row.terminalReport) continue;
|
|
218131
218941
|
let msg;
|
|
218132
218942
|
try {
|
|
218133
218943
|
msg = JSON.parse(row.msg);
|
|
@@ -218144,8 +218954,8 @@ var Daemon = class Daemon {
|
|
|
218144
218954
|
}
|
|
218145
218955
|
return removed;
|
|
218146
218956
|
}
|
|
218147
|
-
isLoopGuardOpen(msg) {
|
|
218148
|
-
return usesLoopGuard(msg) && this.store.isLoopGuardOpen(loopGuardScope(msg));
|
|
218957
|
+
isLoopGuardOpen(msg, includeHook = false) {
|
|
218958
|
+
return (usesLoopGuard(msg) || includeHook) && this.store.isLoopGuardOpen(loopGuardScope(msg));
|
|
218149
218959
|
}
|
|
218150
218960
|
/** First-open side effects for a durable conversation circuit: purge restart work,
|
|
218151
218961
|
* drop every matching serial queue, cancel live ACP turns across ALL agents sharing
|
|
@@ -218212,41 +219022,71 @@ var Daemon = class Daemon {
|
|
|
218212
219022
|
...error ? { error } : {}
|
|
218213
219023
|
});
|
|
218214
219024
|
}
|
|
218215
|
-
dispatch(agentId, msg, integrationId, webchat, callMeta, opts, githubReply) {
|
|
219025
|
+
dispatch(agentId, msg, integrationId, webchat, callMeta, opts, githubReply, hookContext, posterPublishState = githubReply ? "not_started" : void 0) {
|
|
218216
219026
|
return new Promise((resolve, reject) => {
|
|
219027
|
+
let admissionSettled = false;
|
|
219028
|
+
const settleAdmission = (result) => {
|
|
219029
|
+
if (admissionSettled) return;
|
|
219030
|
+
admissionSettled = true;
|
|
219031
|
+
opts?.onAdmission?.(result);
|
|
219032
|
+
};
|
|
218217
219033
|
if (this.draining || this.drainingAgents.has(agentId)) {
|
|
218218
219034
|
this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
|
|
219035
|
+
settleAdmission({
|
|
219036
|
+
accepted: false,
|
|
219037
|
+
reason: "draining"
|
|
219038
|
+
});
|
|
218219
219039
|
resolve(null);
|
|
218220
219040
|
return;
|
|
218221
219041
|
}
|
|
218222
219042
|
if (this.paused(agentId)) {
|
|
218223
219043
|
this.log.info(`dispatch: skipped for agent "${agentId}" (paused)`);
|
|
219044
|
+
settleAdmission({
|
|
219045
|
+
accepted: false,
|
|
219046
|
+
reason: "paused"
|
|
219047
|
+
});
|
|
218224
219048
|
resolve(null);
|
|
218225
219049
|
return;
|
|
218226
219050
|
}
|
|
218227
219051
|
if (this.safetyDrainingAgents.has(agentId)) {
|
|
218228
219052
|
if (opts?.fromInboxReplay) this.waitForSafetyDrain(agentId).then(() => {
|
|
218229
219053
|
if (this.draining || !this.agents.has(agentId)) {
|
|
219054
|
+
settleAdmission({
|
|
219055
|
+
accepted: false,
|
|
219056
|
+
reason: "draining"
|
|
219057
|
+
});
|
|
218230
219058
|
resolve(null);
|
|
218231
219059
|
return;
|
|
218232
219060
|
}
|
|
218233
219061
|
if (opts.inboxReplayId !== void 0 && !this.store.hasInbox(opts.inboxReplayId)) {
|
|
219062
|
+
settleAdmission({
|
|
219063
|
+
accepted: false,
|
|
219064
|
+
reason: "dropped"
|
|
219065
|
+
});
|
|
218234
219066
|
resolve(null);
|
|
218235
219067
|
return;
|
|
218236
219068
|
}
|
|
218237
|
-
this.dispatch(agentId, msg, integrationId, webchat, callMeta, opts, githubReply).then(resolve, reject);
|
|
219069
|
+
this.dispatch(agentId, msg, integrationId, webchat, callMeta, opts, githubReply, hookContext, posterPublishState).then(resolve, reject);
|
|
218238
219070
|
}).catch(reject);
|
|
218239
219071
|
else {
|
|
218240
219072
|
this.log.info(`dispatch: skipped for agent "${agentId}" (stopping interrupted work)`);
|
|
219073
|
+
settleAdmission({
|
|
219074
|
+
accepted: false,
|
|
219075
|
+
reason: "busy"
|
|
219076
|
+
});
|
|
218241
219077
|
resolve(null);
|
|
218242
219078
|
}
|
|
218243
219079
|
return;
|
|
218244
219080
|
}
|
|
218245
219081
|
const key = sessionKey(msg.platform, msg.channel, msg.thread ?? msg.msgId, agentId);
|
|
218246
219082
|
const loopScope = loopGuardScope(msg);
|
|
218247
|
-
if (usesLoopGuard(msg) && this.store.isLoopGuardOpen(loopScope)) {
|
|
219083
|
+
if ((usesLoopGuard(msg) || hookContext !== void 0) && this.store.isLoopGuardOpen(loopScope)) {
|
|
218248
219084
|
if (opts?.fromInboxReplay) this.purgeLoopScopeInbox(loopScope);
|
|
218249
219085
|
this.log.warn(`dispatch: skipped ${msg.msgId}; loop guard is open for ${loopScope}`);
|
|
219086
|
+
settleAdmission({
|
|
219087
|
+
accepted: false,
|
|
219088
|
+
reason: "loop_protection"
|
|
219089
|
+
});
|
|
218250
219090
|
resolve(null);
|
|
218251
219091
|
return;
|
|
218252
219092
|
}
|
|
@@ -218260,6 +219100,10 @@ var Daemon = class Daemon {
|
|
|
218260
219100
|
});
|
|
218261
219101
|
else this.purgeLoopScopeInbox(loopScope);
|
|
218262
219102
|
} else this.log.warn(`dispatch: dropped malformed Slack platform event ${msg.msgId}`);
|
|
219103
|
+
settleAdmission({
|
|
219104
|
+
accepted: false,
|
|
219105
|
+
reason: "malformed_platform_event"
|
|
219106
|
+
});
|
|
218263
219107
|
resolve(null);
|
|
218264
219108
|
return;
|
|
218265
219109
|
}
|
|
@@ -218270,8 +219114,10 @@ var Daemon = class Daemon {
|
|
|
218270
219114
|
...integrationId !== void 0 ? { integrationId } : {},
|
|
218271
219115
|
...webchat ? { webchat } : {},
|
|
218272
219116
|
...callMeta ? { callMeta } : {},
|
|
219117
|
+
...hookContext ? { hookContext } : {},
|
|
218273
219118
|
...opts?.isQueueCmd ? { isQueueCmd: true } : {},
|
|
218274
219119
|
...githubReply ? { githubReply } : {},
|
|
219120
|
+
...posterPublishState ? { posterPublishState } : {},
|
|
218275
219121
|
resolve,
|
|
218276
219122
|
reject
|
|
218277
219123
|
};
|
|
@@ -218279,25 +219125,81 @@ var Daemon = class Daemon {
|
|
|
218279
219125
|
const q = this.serialQueue.get(key) ?? [];
|
|
218280
219126
|
if (q.length >= MAX_QUEUED_PER_SESSION) {
|
|
218281
219127
|
this.log.warn(`dispatch: queue full for session ${key} (${q.length}) — rejecting (queue_full)`);
|
|
219128
|
+
settleAdmission({
|
|
219129
|
+
accepted: false,
|
|
219130
|
+
reason: "queue_full"
|
|
219131
|
+
});
|
|
218282
219132
|
reject(new QueueFullError(key));
|
|
218283
219133
|
return;
|
|
218284
219134
|
}
|
|
218285
219135
|
if (!opts?.replay && !this.admitLoopGuardTurn(agentId, msg, integrationId, opts?.inboxReplayId)) {
|
|
219136
|
+
settleAdmission({
|
|
219137
|
+
accepted: false,
|
|
219138
|
+
reason: "loop_protection"
|
|
219139
|
+
});
|
|
219140
|
+
resolve(null);
|
|
219141
|
+
return;
|
|
219142
|
+
}
|
|
219143
|
+
let persistence;
|
|
219144
|
+
try {
|
|
219145
|
+
persistence = this.persistInbox(entry, key, {
|
|
219146
|
+
required: opts?.requireDurable,
|
|
219147
|
+
adoptExisting: opts?.adoptExistingInbox
|
|
219148
|
+
});
|
|
219149
|
+
} catch (err) {
|
|
219150
|
+
settleAdmission({
|
|
219151
|
+
accepted: false,
|
|
219152
|
+
reason: "durability"
|
|
219153
|
+
});
|
|
219154
|
+
reject(err);
|
|
219155
|
+
return;
|
|
219156
|
+
}
|
|
219157
|
+
if (persistence === "existing") {
|
|
219158
|
+
settleAdmission({
|
|
219159
|
+
accepted: true,
|
|
219160
|
+
duplicate: true
|
|
219161
|
+
});
|
|
218286
219162
|
resolve(null);
|
|
218287
219163
|
return;
|
|
218288
219164
|
}
|
|
218289
219165
|
q.push(entry);
|
|
218290
219166
|
this.serialQueue.set(key, q);
|
|
218291
|
-
|
|
219167
|
+
settleAdmission({ accepted: true });
|
|
218292
219168
|
this.log.debug(`dispatch: queued behind in-flight turn for session ${key} (depth ${q.length})`);
|
|
218293
219169
|
return;
|
|
218294
219170
|
}
|
|
218295
219171
|
if (!opts?.replay && !this.admitLoopGuardTurn(agentId, msg, integrationId, opts?.inboxReplayId)) {
|
|
219172
|
+
settleAdmission({
|
|
219173
|
+
accepted: false,
|
|
219174
|
+
reason: "loop_protection"
|
|
219175
|
+
});
|
|
219176
|
+
resolve(null);
|
|
219177
|
+
return;
|
|
219178
|
+
}
|
|
219179
|
+
let persistence;
|
|
219180
|
+
try {
|
|
219181
|
+
persistence = this.persistInbox(entry, key, {
|
|
219182
|
+
required: opts?.requireDurable,
|
|
219183
|
+
adoptExisting: opts?.adoptExistingInbox
|
|
219184
|
+
});
|
|
219185
|
+
} catch (err) {
|
|
219186
|
+
settleAdmission({
|
|
219187
|
+
accepted: false,
|
|
219188
|
+
reason: "durability"
|
|
219189
|
+
});
|
|
219190
|
+
reject(err);
|
|
219191
|
+
return;
|
|
219192
|
+
}
|
|
219193
|
+
if (persistence === "existing") {
|
|
219194
|
+
settleAdmission({
|
|
219195
|
+
accepted: true,
|
|
219196
|
+
duplicate: true
|
|
219197
|
+
});
|
|
218296
219198
|
resolve(null);
|
|
218297
219199
|
return;
|
|
218298
219200
|
}
|
|
218299
|
-
this.persistInbox(entry, key);
|
|
218300
219201
|
this.inflight.add(key);
|
|
219202
|
+
settleAdmission({ accepted: true });
|
|
218301
219203
|
this.runLoop(key, entry);
|
|
218302
219204
|
});
|
|
218303
219205
|
}
|
|
@@ -218353,7 +219255,7 @@ var Daemon = class Daemon {
|
|
|
218353
219255
|
dispatchGateReason(entry) {
|
|
218354
219256
|
if (entry.cancelledReason) return entry.cancelledReason;
|
|
218355
219257
|
if (this.paused(entry.agentId)) return "pause";
|
|
218356
|
-
if (this.isLoopGuardOpen(entry.msg)) return "loop protection";
|
|
219258
|
+
if (this.isLoopGuardOpen(entry.msg, entry.hookContext !== void 0)) return "loop protection";
|
|
218357
219259
|
}
|
|
218358
219260
|
beginActiveDispatch(agentId, key) {
|
|
218359
219261
|
let resolveDone;
|
|
@@ -218404,13 +219306,19 @@ var Daemon = class Daemon {
|
|
|
218404
219306
|
this.activeGateEntries.set(key, entry);
|
|
218405
219307
|
if (entry !== firstEntry && (this.draining || this.drainingAgents.has(entry.agentId) || this.paused(entry.agentId))) {
|
|
218406
219308
|
this.terminateQueuedSink(entry);
|
|
218407
|
-
if (!this.draining)
|
|
219309
|
+
if (!this.draining) {
|
|
219310
|
+
if (entry.hookContext) this.emitHookCompletion(entry.hookContext, "failed", { reason: "dropped" }, entry);
|
|
219311
|
+
this.removeInbox(entry);
|
|
219312
|
+
}
|
|
218408
219313
|
entry.resolve(null);
|
|
218409
219314
|
const rest = this.serialQueue.get(key) ?? [];
|
|
218410
219315
|
this.serialQueue.delete(key);
|
|
218411
219316
|
for (const e of rest) {
|
|
218412
219317
|
this.terminateQueuedSink(e);
|
|
218413
|
-
if (!this.draining)
|
|
219318
|
+
if (!this.draining) {
|
|
219319
|
+
if (e.hookContext) this.emitHookCompletion(e.hookContext, "failed", { reason: "dropped" }, e);
|
|
219320
|
+
this.removeInbox(e);
|
|
219321
|
+
}
|
|
218414
219322
|
e.resolve(null);
|
|
218415
219323
|
}
|
|
218416
219324
|
this.inflight.delete(key);
|
|
@@ -218434,7 +219342,10 @@ var Daemon = class Daemon {
|
|
|
218434
219342
|
for (const e of rest) {
|
|
218435
219343
|
const failStop = new FailStopError(key);
|
|
218436
219344
|
this.terminateQueuedSink(e, failStop.message);
|
|
218437
|
-
if (!this.draining)
|
|
219345
|
+
if (!this.draining) {
|
|
219346
|
+
if (e.hookContext) this.emitHookCompletion(e.hookContext, "failed", { reason: "fail_stop" }, e);
|
|
219347
|
+
this.removeInbox(e);
|
|
219348
|
+
}
|
|
218438
219349
|
e.reject(failStop);
|
|
218439
219350
|
}
|
|
218440
219351
|
this.inflight.delete(key);
|
|
@@ -218450,15 +219361,26 @@ var Daemon = class Daemon {
|
|
|
218450
219361
|
}
|
|
218451
219362
|
}
|
|
218452
219363
|
async dispatchOne(entry, key) {
|
|
218453
|
-
const { agentId, msg, integrationId, webchat, callMeta, githubReply } = entry;
|
|
219364
|
+
const { agentId, msg, integrationId, webchat, callMeta, githubReply, hookContext } = entry;
|
|
218454
219365
|
const initialGate = this.dispatchGateReason(entry);
|
|
218455
219366
|
if (initialGate) {
|
|
218456
219367
|
this.terminateQueuedSink(entry);
|
|
219368
|
+
if (hookContext && !this.draining) this.emitHookCompletion(hookContext, "failed", { reason: initialGate }, entry);
|
|
218457
219369
|
this.log.info(`dispatch: skipped admitted turn ${msg.msgId} (${initialGate})`);
|
|
218458
219370
|
return null;
|
|
218459
219371
|
}
|
|
219372
|
+
if (hookContext && !hookContext.turnStartedAt) {
|
|
219373
|
+
hookContext.turnStartedAt = new Date(this.clock.now()).toISOString();
|
|
219374
|
+
try {
|
|
219375
|
+
this.persistHookState(entry, void 0, true);
|
|
219376
|
+
} catch (err) {
|
|
219377
|
+
this.emitHookCompletion(hookContext, "failed", { reason: "durability" }, entry);
|
|
219378
|
+
throw err;
|
|
219379
|
+
}
|
|
219380
|
+
}
|
|
218460
219381
|
if (callMeta?.correlationId !== void 0) this.recordWorkerReport(key, callMeta, msg.text);
|
|
218461
219382
|
const agent = this.agents.get(agentId);
|
|
219383
|
+
const agentName = agent.displayName?.trim() || agent.name;
|
|
218462
219384
|
const rec = new TranscriptRecorder();
|
|
218463
219385
|
const replyConn = msg.headless || webchat ? void 0 : this.replyConnFor(agentId, integrationId);
|
|
218464
219386
|
const wasRunning = this.hostStarts.has(agentId);
|
|
@@ -218495,7 +219417,7 @@ var Daemon = class Daemon {
|
|
|
218495
219417
|
this.showActivity(replyConn, msg.channel, statusThread, "");
|
|
218496
219418
|
} else this.surfaceTurnFailure(err, {
|
|
218497
219419
|
agentId,
|
|
218498
|
-
agentName
|
|
219420
|
+
agentName,
|
|
218499
219421
|
platform: msg.platform,
|
|
218500
219422
|
isDm: msg.isDm,
|
|
218501
219423
|
webchat,
|
|
@@ -218507,7 +219429,11 @@ var Daemon = class Daemon {
|
|
|
218507
219429
|
} finally {
|
|
218508
219430
|
releaseReplyConn();
|
|
218509
219431
|
}
|
|
218510
|
-
if (interrupted)
|
|
219432
|
+
if (interrupted) {
|
|
219433
|
+
if (hookContext && !this.draining) this.emitHookCompletion(hookContext, "failed", { reason: interrupted }, entry);
|
|
219434
|
+
return null;
|
|
219435
|
+
}
|
|
219436
|
+
if (hookContext && !this.draining) this.emitHookCompletion(hookContext, "failed", { reason: "session_start_failed" }, entry);
|
|
218511
219437
|
throw err;
|
|
218512
219438
|
}
|
|
218513
219439
|
const { sessionId, blocks, created } = handled;
|
|
@@ -218527,6 +219453,7 @@ var Daemon = class Daemon {
|
|
|
218527
219453
|
this.showActivity(replyConn, msg.channel, statusThread, "");
|
|
218528
219454
|
this.terminateQueuedSink(entry);
|
|
218529
219455
|
releaseReplyConn();
|
|
219456
|
+
if (hookContext && !this.draining) this.emitHookCompletion(hookContext, "failed", { reason: initializedGate }, entry);
|
|
218530
219457
|
this.log.info(`dispatch: skipped initialized turn ${msg.msgId} (${initializedGate})`);
|
|
218531
219458
|
return null;
|
|
218532
219459
|
}
|
|
@@ -218549,7 +219476,7 @@ var Daemon = class Daemon {
|
|
|
218549
219476
|
conv,
|
|
218550
219477
|
rec,
|
|
218551
219478
|
agentId,
|
|
218552
|
-
agentName
|
|
219479
|
+
agentName,
|
|
218553
219480
|
platform: msg.platform,
|
|
218554
219481
|
isDm: msg.isDm,
|
|
218555
219482
|
loopGuardScope: loopGuardScope(msg),
|
|
@@ -218567,13 +219494,17 @@ var Daemon = class Daemon {
|
|
|
218567
219494
|
resolveDone,
|
|
218568
219495
|
...callMeta ? { callMeta } : {},
|
|
218569
219496
|
...pendingWebchat ? { webchat: pendingWebchat } : {},
|
|
218570
|
-
...githubReply ? { github: {
|
|
219497
|
+
...githubReply && entry.posterPublishState !== "in_flight" && entry.posterPublishState !== "settled" ? { github: {
|
|
218571
219498
|
...this.makeGithubReply(agentId, githubReply, sessionId),
|
|
218572
219499
|
deferredFinalTranscript: false
|
|
218573
219500
|
} } : {}
|
|
218574
219501
|
};
|
|
218575
219502
|
this.pending.set(pendingTurnKey(agentId, sessionId), p);
|
|
218576
219503
|
if (callMeta) this.activeTurnCallMeta.set(key, callMeta);
|
|
219504
|
+
const activeGithub = await this.prepareGithubTurn(entry).catch((err) => {
|
|
219505
|
+
this.log.warn(`github review: turn setup failed (${formatErr(err)})`);
|
|
219506
|
+
});
|
|
219507
|
+
if (activeGithub) this.activeGithubTurnMeta.set(key, activeGithub);
|
|
218577
219508
|
let finalPhase = "end";
|
|
218578
219509
|
try {
|
|
218579
219510
|
const host = await this.ensureHostAsync(agentId);
|
|
@@ -218590,6 +219521,10 @@ var Daemon = class Daemon {
|
|
|
218590
219521
|
this.showActivity(replyConn, msg.channel, statusThread, "");
|
|
218591
219522
|
this.terminateQueuedSink(entry);
|
|
218592
219523
|
if (readyGate === "loop protection") finalPhase = "problem";
|
|
219524
|
+
if (hookContext && !this.draining) this.emitHookCompletion(hookContext, "failed", {
|
|
219525
|
+
sessionId,
|
|
219526
|
+
reason: readyGate
|
|
219527
|
+
}, entry);
|
|
218593
219528
|
this.log.info(`dispatch: skipped ready turn ${msg.msgId} (${readyGate})`);
|
|
218594
219529
|
return null;
|
|
218595
219530
|
}
|
|
@@ -218601,6 +219536,10 @@ var Daemon = class Daemon {
|
|
|
218601
219536
|
const { stopReason, usage } = await host.prompt(sessionId, blocks);
|
|
218602
219537
|
if (p.outputSuppressed) {
|
|
218603
219538
|
if (p.outputSuppressed === "loop protection") finalPhase = "problem";
|
|
219539
|
+
if (hookContext && !this.draining) this.emitHookCompletion(hookContext, "failed", {
|
|
219540
|
+
sessionId,
|
|
219541
|
+
reason: p.outputSuppressed
|
|
219542
|
+
}, entry);
|
|
218604
219543
|
return null;
|
|
218605
219544
|
}
|
|
218606
219545
|
if (usage) {
|
|
@@ -218655,13 +219594,17 @@ var Daemon = class Daemon {
|
|
|
218655
219594
|
this.clearIdle(p);
|
|
218656
219595
|
if (p.outputSuppressed) {
|
|
218657
219596
|
if (p.outputSuppressed === "loop protection") finalPhase = "problem";
|
|
219597
|
+
if (hookContext && !this.draining) this.emitHookCompletion(hookContext, "failed", {
|
|
219598
|
+
sessionId,
|
|
219599
|
+
reason: p.outputSuppressed
|
|
219600
|
+
}, entry);
|
|
218658
219601
|
return null;
|
|
218659
219602
|
}
|
|
218660
219603
|
finalPhase = "problem";
|
|
218661
219604
|
if (p.webchat) {
|
|
218662
219605
|
this.surfaceTurnFailure(err, {
|
|
218663
219606
|
agentId,
|
|
218664
|
-
agentName
|
|
219607
|
+
agentName,
|
|
218665
219608
|
platform: msg.platform,
|
|
218666
219609
|
isDm: msg.isDm,
|
|
218667
219610
|
webchat,
|
|
@@ -218692,6 +219635,10 @@ var Daemon = class Daemon {
|
|
|
218692
219635
|
this.showActivity(replyConn, msg.channel, statusThread, "");
|
|
218693
219636
|
await p.applyChain;
|
|
218694
219637
|
}
|
|
219638
|
+
if (hookContext && !this.draining) this.emitHookCompletion(hookContext, "failed", {
|
|
219639
|
+
sessionId,
|
|
219640
|
+
reason: "turn_failed"
|
|
219641
|
+
}, entry);
|
|
218695
219642
|
throw err;
|
|
218696
219643
|
} finally {
|
|
218697
219644
|
this.clearCancelBackstop(agentId, sessionId);
|
|
@@ -218701,6 +219648,7 @@ var Daemon = class Daemon {
|
|
|
218701
219648
|
this.releasePermissions(agentId, sessionId);
|
|
218702
219649
|
this.releaseElicits(agentId, sessionId);
|
|
218703
219650
|
if (callMeta) this.activeTurnCallMeta.delete(key);
|
|
219651
|
+
if (activeGithub && this.activeGithubTurnMeta.get(key) === activeGithub) this.activeGithubTurnMeta.delete(key);
|
|
218704
219652
|
const githubFinal = p.github && !p.outputSuppressed && finalPhase === "end" ? p.github.collector.finalText(true) : void 0;
|
|
218705
219653
|
if (p.github?.deferredFinalTranscript && githubFinal?.trim()) this.store.appendTranscript({
|
|
218706
219654
|
channel: p.channel,
|
|
@@ -218710,7 +219658,19 @@ var Daemon = class Daemon {
|
|
|
218710
219658
|
kind: "text",
|
|
218711
219659
|
text: githubFinal
|
|
218712
219660
|
});
|
|
218713
|
-
if (p.github && !p.outputSuppressed)
|
|
219661
|
+
if (p.github && !p.outputSuppressed) {
|
|
219662
|
+
let recordedInFlight = false;
|
|
219663
|
+
try {
|
|
219664
|
+
this.persistHookState(entry, "in_flight", true);
|
|
219665
|
+
recordedInFlight = true;
|
|
219666
|
+
} catch (err) {
|
|
219667
|
+
this.log.warn(`github poster: durability barrier failed; final publish skipped (${formatErr(err)})`);
|
|
219668
|
+
}
|
|
219669
|
+
if (recordedInFlight) {
|
|
219670
|
+
await p.github.poster.publish(githubFinal).catch((err) => this.log.warn(`github poster: final publish failed (${formatErr(err)})`));
|
|
219671
|
+
this.persistHookState(entry, "settled");
|
|
219672
|
+
}
|
|
219673
|
+
}
|
|
218714
219674
|
this.pending.delete(pendingTurnKey(agentId, sessionId));
|
|
218715
219675
|
this.store.setSessionState(key, "idle", this.clock.now());
|
|
218716
219676
|
this.emitSessionMetadataSnapshot({
|
|
@@ -218723,6 +219683,7 @@ var Daemon = class Daemon {
|
|
|
218723
219683
|
});
|
|
218724
219684
|
p.resolveDone();
|
|
218725
219685
|
}
|
|
219686
|
+
if (hookContext) this.emitHookCompletion(hookContext, "success", { sessionId }, entry);
|
|
218726
219687
|
return sessionId;
|
|
218727
219688
|
}
|
|
218728
219689
|
/** Interrupt a session by its LOGICAL sessionKey (shared by pause, `!stop`, `!cancel`,
|
|
@@ -218744,6 +219705,7 @@ var Daemon = class Daemon {
|
|
|
218744
219705
|
this.serialQueue.delete(key);
|
|
218745
219706
|
for (const e of queued) {
|
|
218746
219707
|
this.terminateQueuedSink(e, opts.dropQueued ? void 0 : reason);
|
|
219708
|
+
if (e.hookContext) this.emitHookCompletion(e.hookContext, "failed", { reason: opts.dropQueued ? "dropped" : "cancelled" }, e);
|
|
218747
219709
|
this.removeInbox(e);
|
|
218748
219710
|
if (opts.dropQueued) e.resolve(null);
|
|
218749
219711
|
else e.reject(new FailStopError(key));
|
|
@@ -220136,49 +221098,92 @@ var Daemon = class Daemon {
|
|
|
220136
221098
|
const purgedPausedAgents = /* @__PURE__ */ new Set();
|
|
220137
221099
|
const purgedLoopScopes = /* @__PURE__ */ new Set();
|
|
220138
221100
|
for (const row of rows) {
|
|
221101
|
+
if (row.completedAt !== null && row.completedAt !== void 0) continue;
|
|
220139
221102
|
if (this.liveInboxIds.has(row.id)) continue;
|
|
220140
221103
|
if (!this.agents.has(row.agentId)) {
|
|
220141
221104
|
this.log.warn(`durable inbox: skipping replay of ${row.id} — unknown agent "${row.agentId}"`);
|
|
220142
221105
|
continue;
|
|
220143
221106
|
}
|
|
220144
|
-
if (this.paused(row.agentId)) {
|
|
220145
|
-
if (!purgedPausedAgents.has(row.agentId)) {
|
|
220146
|
-
purgedPausedAgents.add(row.agentId);
|
|
220147
|
-
this.purgeAgentInbox(row.agentId);
|
|
220148
|
-
}
|
|
220149
|
-
continue;
|
|
220150
|
-
}
|
|
220151
221107
|
let msg;
|
|
220152
221108
|
let callMeta;
|
|
221109
|
+
let hookContext;
|
|
220153
221110
|
try {
|
|
220154
221111
|
msg = JSON.parse(row.msg);
|
|
220155
221112
|
callMeta = row.callMeta ? JSON.parse(row.callMeta) : void 0;
|
|
221113
|
+
hookContext = row.hookContext ? JSON.parse(row.hookContext) : void 0;
|
|
220156
221114
|
} catch (err) {
|
|
220157
221115
|
this.log.warn(`durable inbox: skipping corrupt row ${row.id}: ${err.message}`);
|
|
220158
221116
|
continue;
|
|
220159
221117
|
}
|
|
221118
|
+
if (msg.source === "hook" && !hookContext) {
|
|
221119
|
+
this.log.warn(`durable inbox: tombstoning legacy hook row ${row.id} without trusted context`);
|
|
221120
|
+
this.store.removeInbox(row.id);
|
|
221121
|
+
continue;
|
|
221122
|
+
}
|
|
221123
|
+
if (this.paused(row.agentId)) {
|
|
221124
|
+
if (!purgedPausedAgents.has(row.agentId)) {
|
|
221125
|
+
purgedPausedAgents.add(row.agentId);
|
|
221126
|
+
this.purgeAgentInbox(row.agentId);
|
|
221127
|
+
}
|
|
221128
|
+
if (hookContext) this.emitHookCompletion(hookContext, "failed", { reason: "pause" }, { inboxId: row.id });
|
|
221129
|
+
continue;
|
|
221130
|
+
}
|
|
220160
221131
|
if (isMalformedPlatformTurn(msg) && !msg.isDm) {
|
|
220161
221132
|
this.store.removeInbox(row.id);
|
|
220162
221133
|
continue;
|
|
220163
221134
|
}
|
|
220164
221135
|
const loopScope = loopGuardScope(msg);
|
|
220165
|
-
if (usesLoopGuard(msg) && this.store.isLoopGuardOpen(loopScope)) {
|
|
221136
|
+
if ((usesLoopGuard(msg) || hookContext !== void 0) && this.store.isLoopGuardOpen(loopScope)) {
|
|
220166
221137
|
if (!purgedLoopScopes.has(loopScope)) {
|
|
220167
221138
|
purgedLoopScopes.add(loopScope);
|
|
220168
221139
|
this.purgeLoopScopeInbox(loopScope);
|
|
220169
221140
|
}
|
|
221141
|
+
if (hookContext) this.emitHookCompletion(hookContext, "failed", { reason: "loop protection" }, { inboxId: row.id });
|
|
220170
221142
|
continue;
|
|
220171
221143
|
}
|
|
221144
|
+
const posterPublishState = row.posterPublishState === "in_flight" || row.posterPublishState === "settled" ? row.posterPublishState : hookContext?.githubReply ? "not_started" : void 0;
|
|
220172
221145
|
this.dispatch(row.agentId, msg, row.integrationId ?? void 0, void 0, callMeta, {
|
|
220173
221146
|
...row.isQueueCmd ? { isQueueCmd: true } : {},
|
|
220174
221147
|
fromInboxReplay: true,
|
|
220175
221148
|
inboxReplayId: row.id,
|
|
220176
|
-
replay: row.loopGuardCounted === 1
|
|
220177
|
-
|
|
221149
|
+
replay: row.loopGuardCounted === 1,
|
|
221150
|
+
adoptExistingInbox: true,
|
|
221151
|
+
...hookContext ? { requireDurable: true } : {}
|
|
221152
|
+
}, hookContext?.githubReply, hookContext, posterPublishState).catch(() => {});
|
|
220178
221153
|
replayed++;
|
|
220179
221154
|
}
|
|
220180
221155
|
if (replayed) this.log.info(`durable inbox: replayed ${replayed} admitted message(s) through the serial gate`);
|
|
220181
221156
|
}
|
|
221157
|
+
/** Re-assert retained metadata-only hook completions whenever the CP socket
|
|
221158
|
+
* becomes ready. HookRepo applies them idempotently; keeping the receipt also
|
|
221159
|
+
* closes the completed-before-restart relay redelivery window. */
|
|
221160
|
+
replayHookTerminalReports() {
|
|
221161
|
+
let rows;
|
|
221162
|
+
try {
|
|
221163
|
+
rows = this.store.listInboxBySessionKeyFifo();
|
|
221164
|
+
} catch (err) {
|
|
221165
|
+
this.log.warn(`durable inbox: terminal report read failed: ${formatErr(err)}`);
|
|
221166
|
+
this.scheduleHookReportRetry();
|
|
221167
|
+
return;
|
|
221168
|
+
}
|
|
221169
|
+
const pending = rows.filter((row) => row.terminalReport && row.legacyReportConnection !== this.hookReportConnectionId && !this.hookReportInflight.has(row.id));
|
|
221170
|
+
const available = Math.max(0, MAX_HOOK_REPORT_INFLIGHT - this.hookReportInflight.size);
|
|
221171
|
+
const batch = pending.slice(0, available);
|
|
221172
|
+
let emitted = 0;
|
|
221173
|
+
for (const row of batch) try {
|
|
221174
|
+
const decoded = HookReport.parse(JSON.parse(row.terminalReport));
|
|
221175
|
+
this.sendHookReport(decoded, row.id);
|
|
221176
|
+
emitted += 1;
|
|
221177
|
+
} catch (err) {
|
|
221178
|
+
this.log.warn(`durable inbox: corrupt terminal hook receipt ${row.id}: ${formatErr(err)}`);
|
|
221179
|
+
try {
|
|
221180
|
+
this.store.acknowledgeHookInbox(row.id);
|
|
221181
|
+
} catch (cleanupError) {
|
|
221182
|
+
this.log.warn(`durable inbox: corrupt receipt cleanup failed for ${row.id}: ${formatErr(cleanupError)}`);
|
|
221183
|
+
}
|
|
221184
|
+
}
|
|
221185
|
+
if (emitted) this.log.info(`durable inbox: re-emitted ${emitted} terminal hook report(s)`);
|
|
221186
|
+
}
|
|
220182
221187
|
/** daemon/restart + daemon/upgrade (§8.3): ack now, then drain + stop + exit so
|
|
220183
221188
|
* the supervisor relaunches (the new binary, for upgrade). */
|
|
220184
221189
|
scheduleFleetExit(kind, targetVersion) {
|
|
@@ -220435,7 +221440,7 @@ var Daemon = class Daemon {
|
|
|
220435
221440
|
* still runs; a `headless` msg additionally suppresses all platform output
|
|
220436
221441
|
* in dispatch. Resolves with dispatch's sessionId (null = gate-dropped).
|
|
220437
221442
|
*/
|
|
220438
|
-
async
|
|
221443
|
+
async anchorTrigger(agentId, msg, target, anchorText, label) {
|
|
220439
221444
|
if (this.draining || this.drainingAgents.has(agentId) || this.paused(agentId) || this.safetyDrainingAgents.has(agentId)) {
|
|
220440
221445
|
this.log.info(`${label}: skipped for agent "${agentId}" (paused or draining)`);
|
|
220441
221446
|
return null;
|
|
@@ -220453,7 +221458,12 @@ var Daemon = class Daemon {
|
|
|
220453
221458
|
this.log.warn(`${label}: failed to post trigger to ${target.channel} (${formatErr(err)}) — running without anchor`);
|
|
220454
221459
|
}
|
|
220455
221460
|
}
|
|
220456
|
-
return
|
|
221461
|
+
return msg;
|
|
221462
|
+
}
|
|
221463
|
+
async fireTrigger(agentId, msg, target, anchorText, label, githubReply, hookContext) {
|
|
221464
|
+
const anchored = await this.anchorTrigger(agentId, msg, target, anchorText, label);
|
|
221465
|
+
if (!anchored) return null;
|
|
221466
|
+
return this.dispatch(agentId, anchored, target?.integrationId, void 0, void 0, void 0, githubReply, hookContext);
|
|
220457
221467
|
}
|
|
220458
221468
|
/** Build the per-turn GitHub final-answer selector and poster, tokened
|
|
220459
221469
|
* via the repo-targeted gitcred mint (issues/PR write, no contents — never
|
|
@@ -220587,6 +221597,8 @@ var Daemon = class Daemon {
|
|
|
220587
221597
|
mcpServerFacts: () => this.mcpServerFacts,
|
|
220588
221598
|
onReady: () => {
|
|
220589
221599
|
this.probeRuntimesAndEmit();
|
|
221600
|
+
this.hookReportConnectionId = randomUUID();
|
|
221601
|
+
this.replayHookTerminalReports();
|
|
220590
221602
|
for (const [integrationId, channels] of this.channelSnapshots) this.cpClient?.emitIntegrationChannels({
|
|
220591
221603
|
integrationId,
|
|
220592
221604
|
channels
|
|
@@ -220708,6 +221720,10 @@ var Daemon = class Daemon {
|
|
|
220708
221720
|
this.coldCancelTimers.clear();
|
|
220709
221721
|
for (const t of this.slackRetryTimers.values()) this.clock.clearTimeout(t);
|
|
220710
221722
|
this.slackRetryTimers.clear();
|
|
221723
|
+
if (this.hookReportRetryTimer !== void 0) {
|
|
221724
|
+
this.clock.clearTimeout(this.hookReportRetryTimer);
|
|
221725
|
+
this.hookReportRetryTimer = void 0;
|
|
221726
|
+
}
|
|
220711
221727
|
for (const t of this.orchestrationDeadlines.values()) this.clock.clearTimeout(t);
|
|
220712
221728
|
this.orchestrationDeadlines.clear();
|
|
220713
221729
|
this.metrics?.stop();
|
|
@@ -220769,4 +221785,4 @@ async function runForeground(opts, deps = {}) {
|
|
|
220769
221785
|
//#endregion
|
|
220770
221786
|
export { ClientTransport as i, CP_SUBPROTOCOL as n, CP_WS_PATH as r, runForeground as t };
|
|
220771
221787
|
|
|
220772
|
-
//# sourceMappingURL=run-foreground-
|
|
221788
|
+
//# sourceMappingURL=run-foreground-BLhVJZlp.js.map
|