@integrity-labs/agt-cli 0.28.621 → 0.28.622

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/bin/agt.js CHANGED
@@ -40,10 +40,10 @@ import {
40
40
  success,
41
41
  table,
42
42
  warn
43
- } from "../chunk-MSQXSVA5.js";
43
+ } from "../chunk-6TLALALT.js";
44
44
  import {
45
45
  readDirectChatSessionState
46
- } from "../chunk-QSPSXG4K.js";
46
+ } from "../chunk-UIB2SMFE.js";
47
47
  import {
48
48
  AnchorSessionClient,
49
49
  CHANNEL_REGISTRY,
@@ -4909,7 +4909,7 @@ import { execFileSync, execSync } from "child_process";
4909
4909
  import { existsSync as existsSync10, realpathSync as realpathSync2 } from "fs";
4910
4910
  import chalk18 from "chalk";
4911
4911
  import ora16 from "ora";
4912
- var cliVersion = true ? "0.28.621" : "dev";
4912
+ var cliVersion = true ? "0.28.622" : "dev";
4913
4913
  async function fetchLatestVersion() {
4914
4914
  const host2 = getHost();
4915
4915
  if (!host2) return null;
@@ -6089,7 +6089,7 @@ function handleError(err) {
6089
6089
  }
6090
6090
 
6091
6091
  // src/bin/agt.ts
6092
- var cliVersion2 = true ? "0.28.621" : "dev";
6092
+ var cliVersion2 = true ? "0.28.622" : "dev";
6093
6093
  var program = new Command();
6094
6094
  program.name("agt").description("Augmented CLI \u2014 agent provisioning and management").version(cliVersion2).option("--json", "Emit machine-readable JSON output (suppress spinners and colors)").option("--skip-update-check", "Skip the automatic update check on startup");
6095
6095
  program.hook("preAction", async (thisCommand, actionCommand) => {
@@ -7,7 +7,7 @@ import {
7
7
  expandTemplateVars,
8
8
  isolationMode,
9
9
  parseEnvIntegrations
10
- } from "./chunk-QSPSXG4K.js";
10
+ } from "./chunk-UIB2SMFE.js";
11
11
  import {
12
12
  BIND_FAILURE_QUARANTINE_THRESHOLD,
13
13
  INTEGRATIONS_SECTION_END,
@@ -6065,7 +6065,7 @@ function exchangeFailureKind(err) {
6065
6065
  }
6066
6066
 
6067
6067
  // src/lib/api-client.ts
6068
- var agtCliVersion = true ? "0.28.621" : "dev";
6068
+ var agtCliVersion = true ? "0.28.622" : "dev";
6069
6069
  var lastConfigHash = null;
6070
6070
  function setConfigHash(hash) {
6071
6071
  lastConfigHash = hash && hash.length > 0 ? hash : null;
@@ -9558,4 +9558,4 @@ export {
9558
9558
  managerInstallSystemUnitCommand,
9559
9559
  managerUninstallSystemUnitCommand
9560
9560
  };
9561
- //# sourceMappingURL=chunk-MSQXSVA5.js.map
9561
+ //# sourceMappingURL=chunk-6TLALALT.js.map
@@ -848,7 +848,6 @@ function buildOpencodeIntegrationServers(integrations, ctx) {
848
848
  }
849
849
 
850
850
  // ../../packages/core/dist/provisioning/frameworks/opencode/opencode-client.js
851
- var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
852
851
  var HttpOpencodeClient = class _HttpOpencodeClient {
853
852
  base;
854
853
  headers;
@@ -895,68 +894,44 @@ var HttpOpencodeClient = class _HttpOpencodeClient {
895
894
  }
896
895
  }
897
896
  async createSession(params) {
898
- const out = await this.call("POST", "/api/session", params ?? {});
899
- const id = out.data?.id;
897
+ const out = await this.call("POST", "/session", params ?? {});
898
+ const id = out.id;
900
899
  if (!id)
901
900
  throw new Error("opencode createSession returned no session id");
902
901
  return { sessionID: id };
903
902
  }
904
- async prompt(params) {
905
- const out = await this.call("POST", `/api/session/${params.sessionID}/prompt`, {
906
- prompt: { text: params.text },
907
- // ENG-7931: pass the model on the prompt so the serve runs the intended
908
- // provider/model instead of defaulting the session to a free Zen model.
909
- ...params.model ? { model: params.model } : {},
910
- delivery: params.delivery ?? "queue"
903
+ async sendMessage(params) {
904
+ const out = await this.call("POST", `/session/${params.sessionID}/message`, {
905
+ parts: [{ type: "text", text: params.text }],
906
+ ...params.model ? { model: { providerID: params.model.providerID, modelID: params.model.id } } : {}
911
907
  });
912
- return { admittedSeq: out.data?.admittedSeq ?? 0, messageID: out.data?.id };
913
- }
914
- async waitIdle(sessionID, opts = {}) {
915
- const timeoutMs = opts.timeoutMs ?? 12e4;
916
- const pollIntervalMs = opts.pollIntervalMs ?? 300;
917
- const deadline = Date.now() + timeoutMs;
918
- for (; ; ) {
919
- const newest = _HttpOpencodeClient.newestByCreated(await this.fetchMessages(sessionID));
920
- if (newest?.type === "assistant" && newest.time?.completed != null)
921
- return;
922
- if (Date.now() >= deadline) {
923
- throw new Error(`opencode waitIdle: session ${sessionID} did not go idle within ${timeoutMs}ms`);
924
- }
925
- await delay(pollIntervalMs);
926
- }
908
+ const reply = _HttpOpencodeClient.textOf(out.parts ?? []);
909
+ return { reply, messageID: out.info?.id, parentID: out.info?.parentID };
927
910
  }
928
- async latestAssistantText(sessionID) {
929
- const assistants = (await this.fetchMessages(sessionID)).filter((m) => m.type === "assistant");
930
- const newest = _HttpOpencodeClient.newestByCreated(assistants);
931
- if (!newest)
932
- return null;
933
- const text = (newest.content ?? []).filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
911
+ /** Concatenated text of the `text` parts of one message, or null if none. */
912
+ static textOf(parts) {
913
+ const text = parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
934
914
  return text || null;
935
915
  }
936
916
  async fetchMessages(sessionID) {
937
- const out = await this.call("GET", `/api/session/${sessionID}/message`);
938
- return out.data ?? [];
917
+ const out = await this.call("GET", `/session/${sessionID}/message`);
918
+ if (!Array.isArray(out))
919
+ return [];
920
+ return out.map((m) => ({
921
+ id: m.info?.id,
922
+ type: m.info?.role,
923
+ time: m.info?.time,
924
+ finish: m.info?.finish,
925
+ content: m.parts ?? []
926
+ }));
939
927
  }
940
928
  async listSessions() {
941
- const out = await this.call("GET", "/api/session");
942
- return (out.data ?? []).filter((s) => typeof s.id === "string").map((s) => ({ id: s.id, title: s.title, updated: s.time?.updated, created: s.time?.created }));
929
+ const out = await this.call("GET", "/session");
930
+ return (Array.isArray(out) ? out : []).filter((s) => typeof s.id === "string").map((s) => ({ id: s.id, title: s.title, updated: s.time?.updated, created: s.time?.created }));
943
931
  }
944
932
  async getStructuredMessages(sessionID) {
945
933
  return this.fetchMessages(sessionID);
946
934
  }
947
- /** The message with the greatest `time.created` (opencode returns newest-first, but don't rely on order). */
948
- static newestByCreated(messages) {
949
- let newest;
950
- let newestT = Number.NEGATIVE_INFINITY;
951
- for (const m of messages) {
952
- const t = m.time?.created ?? 0;
953
- if (t >= newestT) {
954
- newestT = t;
955
- newest = m;
956
- }
957
- }
958
- return newest;
959
- }
960
935
  };
961
936
 
962
937
  // ../../packages/core/dist/provisioning/frameworks/opencode/opencode-transcript.js
@@ -1025,123 +1000,13 @@ function buildPart(p, redact, maxPartChars) {
1025
1000
  return { kind: "other" };
1026
1001
  }
1027
1002
 
1028
- // ../../packages/core/dist/provisioning/frameworks/opencode/opencode-run.js
1029
- var OpencodeRunError = class extends Error {
1030
- admitted;
1031
- constructor(message, admitted, options) {
1032
- super(message, options);
1033
- this.name = "OpencodeRunError";
1034
- this.admitted = admitted;
1035
- }
1036
- };
1037
- function runCrossedAdmission(stdout) {
1038
- for (const line2 of stdout.split("\n")) {
1039
- const trimmed = line2.trim();
1040
- if (!trimmed)
1041
- continue;
1042
- try {
1043
- JSON.parse(trimmed);
1044
- return true;
1045
- } catch {
1046
- }
1047
- }
1048
- return false;
1049
- }
1050
- function buildOpencodeRunArgs(params, opts = {}) {
1051
- const args = [
1052
- "run",
1053
- "--attach",
1054
- params.serveUrl,
1055
- "--dir",
1056
- params.projectDir,
1057
- "--session",
1058
- params.sessionID,
1059
- "--format",
1060
- "json"
1061
- ];
1062
- if (opts.includePasswordArg && params.password) {
1063
- args.push("-p", params.password);
1064
- }
1065
- if (params.model) {
1066
- args.push("--model", `${params.model.providerID}/${params.model.id}`);
1067
- }
1068
- if (params.agent) {
1069
- args.push("--agent", params.agent);
1070
- }
1071
- args.push(params.text);
1072
- return args;
1073
- }
1074
- function parseOpencodeRunReply(stdout) {
1075
- const parts = [];
1076
- for (const line2 of stdout.split("\n")) {
1077
- const trimmed = line2.trim();
1078
- if (!trimmed)
1079
- continue;
1080
- let evt;
1081
- try {
1082
- evt = JSON.parse(trimmed);
1083
- } catch {
1084
- continue;
1085
- }
1086
- const e = evt;
1087
- if (e?.type === "text" && e.part && e.part.type === "text" && typeof e.part.text === "string") {
1088
- parts.push(e.part.text);
1089
- }
1090
- }
1091
- const reply = parts.join("").trim();
1092
- return reply.length > 0 ? reply : null;
1093
- }
1094
- function parseOpencodeRunError(stdout) {
1095
- for (const line2 of stdout.split("\n")) {
1096
- const trimmed = line2.trim();
1097
- if (!trimmed)
1098
- continue;
1099
- let evt;
1100
- try {
1101
- evt = JSON.parse(trimmed);
1102
- } catch {
1103
- continue;
1104
- }
1105
- const e = evt;
1106
- if (e?.type === "error") {
1107
- const msg = e.error?.data?.message;
1108
- if (typeof msg === "string" && msg.length > 0)
1109
- return msg;
1110
- const name = e.error?.name;
1111
- if (typeof name === "string" && name.length > 0)
1112
- return name;
1113
- return "opencode run reported an error event";
1114
- }
1115
- }
1116
- return null;
1117
- }
1118
- function parseRunToolCalls(stdout) {
1119
- const seen = /* @__PURE__ */ new Set();
1120
- const out = [];
1121
- for (const line2 of stdout.split("\n")) {
1122
- const trimmed = line2.trim();
1123
- if (!trimmed)
1124
- continue;
1125
- let evt;
1126
- try {
1127
- evt = JSON.parse(trimmed);
1128
- } catch {
1129
- continue;
1130
- }
1131
- const e = evt;
1132
- if (e.part && e.part.type === "tool" && typeof e.part.tool === "string") {
1133
- if (!seen.has(e.part.tool)) {
1134
- seen.add(e.part.tool);
1135
- out.push(e.part.tool);
1136
- }
1137
- }
1138
- }
1139
- return out;
1140
- }
1141
-
1142
1003
  // ../../packages/core/dist/provisioning/frameworks/opencode/inbound-bridge.js
1143
1004
  var InboundError = class extends Error {
1144
- /** True once `prompt()` has durably admitted the turn. Retry is unsafe. */
1005
+ /**
1006
+ * False ONLY when the turn provably never reached the agent (createSession
1007
+ * failed). A failure once the message is in flight is reported as `true`: the
1008
+ * turn probably ran, so retrying it would duplicate it.
1009
+ */
1145
1010
  admitted;
1146
1011
  sessionID;
1147
1012
  cause;
@@ -1171,24 +1036,64 @@ var OpencodeInboundBridge = class {
1171
1036
  client;
1172
1037
  gate;
1173
1038
  sessionDefaults;
1174
- delivery;
1175
1039
  awaitReply;
1176
- runTurn;
1177
1040
  /** conversationKey → sessionID (one session per thread/DM). */
1178
1041
  sessions = /* @__PURE__ */ new Map();
1179
1042
  /** conversationKey → in-flight createSession, so concurrent inbound for the
1180
1043
  * same conversation share one session instead of racing to create two. */
1181
1044
  inFlight = /* @__PURE__ */ new Map();
1045
+ /**
1046
+ * conversationKey → tail of the turn chain for that conversation. Turns on one
1047
+ * conversation run STRICTLY ONE AT A TIME.
1048
+ *
1049
+ * This is load-bearing for correctness, not a politeness measure (ENG-9096).
1050
+ * opencode queues overlapping turns correctly - the transcript comes out in
1051
+ * order and nothing is lost - but every concurrent `POST /session/{id}/message`
1052
+ * returns the SAME assistant message: the newest one, not the caller's own.
1053
+ * Measured: two overlapping turns, and the POST that submitted the FIRST
1054
+ * message received the reply to the SECOND, while the first turn's reply was
1055
+ * returned to nobody. On a channel that means one person being handed the
1056
+ * answer to someone else's question in the same thread.
1057
+ *
1058
+ * The response gives no way to detect this after the fact: it carries
1059
+ * `info.parentID` (the user message it answers) but the caller is never told
1060
+ * its own user-message id, so there is nothing to compare against. Serialising
1061
+ * is therefore the fix, not a mitigation - the alternative is undetectable.
1062
+ *
1063
+ * `opencode run --attach` did not need this: each spawn streamed only its own
1064
+ * turn, so correlation came free. This is the one property the subprocess had
1065
+ * that the HTTP path does not.
1066
+ */
1067
+ turnChains = /* @__PURE__ */ new Map();
1182
1068
  constructor(opts) {
1183
1069
  this.client = opts.client;
1184
1070
  this.gate = opts.gate ?? (() => ({ admit: true }));
1185
1071
  this.sessionDefaults = opts.sessionDefaults;
1186
- this.delivery = opts.delivery ?? "queue";
1187
1072
  this.awaitReply = opts.awaitReply ?? true;
1188
- this.runTurn = opts.runTurn;
1189
- if (this.runTurn && this.delivery === "steer") {
1190
- console.warn("[opencode-bridge] delivery:'steer' is ignored when runTurn is set - opencode run cannot steer an in-flight turn; falling back to queue semantics.");
1191
- }
1073
+ }
1074
+ /**
1075
+ * Run `fn` only once every previously-queued turn for `conversationKey` has
1076
+ * settled.
1077
+ *
1078
+ * `then(fn, fn)` — the SAME handler on both arms — is what keeps the chain
1079
+ * alive: a predecessor that rejected still lets the next turn run, so one
1080
+ * failed turn cannot strand every later message in that conversation. The
1081
+ * promise returned to the caller is that same one, so the caller still sees
1082
+ * the real failure.
1083
+ *
1084
+ * There was a second, redundant guard here (storing a tail wrapped to always
1085
+ * resolve). It was removed deliberately. With both present, sabotaging EITHER
1086
+ * one alone still passed every test, because the other silently covered it —
1087
+ * so neither was actually protected by anything. One mechanism, one test that
1088
+ * fails when it goes. Storing the rejecting promise raises no unhandled
1089
+ * rejection, because it is the same object the caller already handled; that
1090
+ * was measured, not assumed.
1091
+ */
1092
+ serialize(conversationKey, fn) {
1093
+ const prior = this.turnChains.get(conversationKey) ?? Promise.resolve();
1094
+ const next = prior.then(fn, fn);
1095
+ this.turnChains.set(conversationKey, next);
1096
+ return next;
1192
1097
  }
1193
1098
  /** Resolve (creating on first use) the opencode session for a conversation. */
1194
1099
  async ensureSession(conversationKey) {
@@ -1238,55 +1143,19 @@ var OpencodeInboundBridge = class {
1238
1143
  cause: err
1239
1144
  });
1240
1145
  }
1241
- if (this.runTurn) {
1242
- const runner = this.runTurn;
1243
- const framed = frameInboundPrompt(msg);
1244
- const model = this.sessionDefaults?.model ?? null;
1245
- if (!awaitReply) {
1246
- void runner({ sessionID, text: framed, model }).catch((err) => {
1247
- console.error(`[opencode-bridge] fire-and-forget run turn failed for session ${sessionID}:`, err instanceof Error ? err.name : typeof err);
1248
- });
1249
- return { status: "admitted", sessionID, admittedSeq: 0 };
1250
- }
1251
- try {
1252
- const { admittedSeq: seq, reply } = await runner({ sessionID, text: framed, model });
1253
- return { status: "replied", sessionID, admittedSeq: seq, reply };
1254
- } catch (err) {
1255
- const admitted = err instanceof OpencodeRunError ? err.admitted : false;
1256
- throw new InboundError(admitted ? "opencode run turn failed after admission (not retried; avoids duplicate)" : "opencode run turn failed before admission (treated as retryable)", { admitted, sessionID, cause: err });
1257
- }
1258
- }
1259
- let admittedSeq;
1260
- try {
1261
- ({ admittedSeq } = await this.client.prompt({
1262
- sessionID,
1263
- text: frameInboundPrompt(msg),
1264
- delivery: this.delivery,
1265
- // ENG-7931: the serve does not apply the config default model to
1266
- // API-created sessions, so run each turn on the explicitly-resolved
1267
- // model (same one used for session-create).
1268
- model: this.sessionDefaults?.model
1269
- }));
1270
- } catch (err) {
1271
- throw new InboundError("opencode prompt failed (admit ambiguous, treated as retryable)", {
1272
- admitted: false,
1273
- sessionID,
1274
- cause: err
1275
- });
1276
- }
1146
+ const framed = frameInboundPrompt(msg);
1147
+ const model = this.sessionDefaults?.model;
1277
1148
  if (!awaitReply) {
1278
- return { status: "admitted", sessionID, admittedSeq };
1149
+ void this.serialize(msg.conversationKey, () => this.client.sendMessage({ sessionID, text: framed, model })).catch((err) => {
1150
+ console.error(`[opencode-bridge] fire-and-forget turn failed for session ${sessionID}:`, err instanceof Error ? err.name : typeof err);
1151
+ });
1152
+ return { status: "admitted", sessionID };
1279
1153
  }
1280
1154
  try {
1281
- await this.client.waitIdle(sessionID);
1282
- const reply = await this.client.latestAssistantText(sessionID);
1283
- return { status: "replied", sessionID, admittedSeq, reply };
1155
+ const { reply } = await this.serialize(msg.conversationKey, () => this.client.sendMessage({ sessionID, text: framed, model }));
1156
+ return { status: "replied", sessionID, reply };
1284
1157
  } catch (err) {
1285
- throw new InboundError("opencode reply read failed AFTER a durable admit (do not retry)", {
1286
- admitted: true,
1287
- sessionID,
1288
- cause: err
1289
- });
1158
+ throw new InboundError("opencode turn failed; the turn may have run to completion regardless, so it is not redelivered", { admitted: true, sessionID, cause: err });
1290
1159
  }
1291
1160
  }
1292
1161
  };
@@ -2385,76 +2254,22 @@ function noteOpencodeTurnOutcome(codeName, outcome, startedOn) {
2385
2254
  function getBridge(codeName, port, password) {
2386
2255
  const cached = bridges.get(codeName);
2387
2256
  if (cached && cached.port === port && cached.password === password) return cached.bridge;
2388
- const client = new HttpOpencodeClient({ baseUrl: baseUrlFor(port), password });
2257
+ const client = new HttpOpencodeClient({
2258
+ baseUrl: baseUrlFor(port),
2259
+ password,
2260
+ // A turn now runs inside one request, so the request timeout IS the turn
2261
+ // budget (see OPENCODE_RUN_TIMEOUT_MS). Leaving the client's 120s default
2262
+ // would quietly shorten it.
2263
+ requestTimeoutMs: OPENCODE_RUN_TIMEOUT_MS
2264
+ });
2389
2265
  const model = sessions.get(codeName)?.model ?? null;
2390
- const projectDir = sessions.get(codeName)?.projectDir ?? null;
2391
2266
  const bridge = new OpencodeInboundBridge({
2392
2267
  client,
2393
- sessionDefaults: model ? { model } : void 0,
2394
- runTurn: projectDir ? makeRunTurn(codeName, port, password, projectDir) : void 0
2268
+ sessionDefaults: model ? { model } : void 0
2395
2269
  });
2396
2270
  bridges.set(codeName, { port, password, bridge });
2397
2271
  return bridge;
2398
2272
  }
2399
- function makeRunTurn(codeName, port, password, projectDir) {
2400
- return ({ sessionID, text, model }) => new Promise((resolve, reject) => {
2401
- const args = buildOpencodeRunArgs({
2402
- bin: OPENCODE_BIN,
2403
- serveUrl: baseUrlFor(port),
2404
- projectDir,
2405
- password,
2406
- sessionID,
2407
- text,
2408
- model: model ?? null
2409
- });
2410
- const child = spawn(OPENCODE_BIN, args, {
2411
- cwd: projectDir,
2412
- // Password off argv (would show in `ps`); it rides the env instead.
2413
- env: { ...process.env, OPENCODE_SERVER_PASSWORD: password },
2414
- stdio: ["ignore", "pipe", "pipe"]
2415
- });
2416
- let stdout = "";
2417
- let stderr = "";
2418
- let settled = false;
2419
- const finish = (fn) => {
2420
- if (settled) return;
2421
- settled = true;
2422
- clearTimeout(timer);
2423
- fn();
2424
- };
2425
- const fail = (message, cause) => reject(new OpencodeRunError(message, runCrossedAdmission(stdout), { cause }));
2426
- const timer = setTimeout(() => {
2427
- finish(() => {
2428
- try {
2429
- child.kill("SIGKILL");
2430
- } catch {
2431
- }
2432
- fail(`opencode run timed out after ${OPENCODE_RUN_TIMEOUT_MS}ms`);
2433
- });
2434
- }, OPENCODE_RUN_TIMEOUT_MS);
2435
- child.stdout.on("data", (d) => {
2436
- stdout += d.toString();
2437
- });
2438
- child.stderr.on("data", (d) => {
2439
- stderr += d.toString();
2440
- });
2441
- child.on("error", (err) => finish(() => fail(`opencode run spawn failed: ${err instanceof Error ? err.message : err}`, err)));
2442
- child.on("close", (code) => finish(() => {
2443
- const runError = parseOpencodeRunError(stdout);
2444
- if (code !== 0 || runError) {
2445
- const tail = runError ?? (stderr.trim() || stdout.trim()).slice(-500);
2446
- fail(`opencode run failed (exit ${code ?? "null"}): ${tail}`);
2447
- return;
2448
- }
2449
- const reply = parseOpencodeRunReply(stdout);
2450
- const tools = parseRunToolCalls(stdout);
2451
- if (tools.length > 0) {
2452
- console.error(`[opencode-run] ${codeName} tools: ${tools.join(", ")}`);
2453
- }
2454
- resolve({ admittedSeq: 0, reply });
2455
- }));
2456
- });
2457
- }
2458
2273
  function stopOpencodeSession(codeName, log2) {
2459
2274
  const tmuxSession = opencodeTmuxSession(codeName);
2460
2275
  try {
@@ -4352,4 +4167,4 @@ export {
4352
4167
  stopAllSessionsAndWait,
4353
4168
  getProjectDir
4354
4169
  };
4355
- //# sourceMappingURL=chunk-QSPSXG4K.js.map
4170
+ //# sourceMappingURL=chunk-UIB2SMFE.js.map