@cohortapp/agent-sdk 2.11.6 → 2.11.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cohortapp/agent-sdk",
3
- "version": "2.11.6",
3
+ "version": "2.11.8",
4
4
  "description": "Cohort Agent SDK — autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -84,7 +84,7 @@ const RUNG_MECHANISM = Object.fromEntries(RUNGS.map((r) => [r.id, r.mechanism]))
84
84
  // work and land it in the org's shared, ACL'd store so the fleet's memory
85
85
  // compounds. Fail-open + best-effort — never blocks or throws into the hot path;
86
86
  // local SQLite/interaction logs remain the fail-open cache.
87
- import { isEnabled as orgEnabled, loadOrgConfig, configFromAgent, electResponder } from "../../lib/org/client.mjs";
87
+ import { isEnabled as orgEnabled, loadOrgConfig, configFromAgent, electResponder, fetchSelfProfile } from "../../lib/org/client.mjs";
88
88
  import { remember as orgRemember } from "../../lib/org/knowledge.mjs";
89
89
  import { sweepSessionOutcomes, resultTextFromStdout } from "./session-outcomes.mjs";
90
90
  import { recordWorkStep as orgRecordWorkStep, cohortChannelId, cohortMessageId } from "../../lib/org/work-ledger.mjs";
@@ -1085,8 +1085,83 @@ export function _resetLadderCaches() {
1085
1085
  _ladderDisabledWarned = false;
1086
1086
  }
1087
1087
 
1088
+ // The seat's own member id (cuid), resolved once. This is the `actor` on every
1089
+ // message/event the seat sends, so the own-echo guard needs it — NOT the slug
1090
+ // (e.g. "A016"), which never matches a cuid actor. COHORT_AGENT_ID is a slug, so
1091
+ // using it as `me` (the old behaviour) left own-echo permanently broken: the seat
1092
+ // re-ingested its own replies and held a conversation with itself.
1093
+ let _selfMemberId = null;
1094
+
1095
+ /** Read a previously-persisted memberId (cuid) from config/agent.json. */
1096
+ function persistedSelfMemberId() {
1097
+ try {
1098
+ const c = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config", "agent.json"), "utf-8"));
1099
+ const mid = c && typeof c.memberId === "string" ? c.memberId : "";
1100
+ // Guard against a slug ever being persisted here — the field MUST be a cuid.
1101
+ return mid && mid !== process.env.COHORT_AGENT_ID ? mid : "";
1102
+ } catch {
1103
+ return "";
1104
+ }
1105
+ }
1106
+
1107
+ /**
1108
+ * Resolve + cache the seat's own member cuid via /whoami (org/members/<slug>),
1109
+ * persisting it to config/agent.json so subsequent boots — and every own-echo
1110
+ * check — have it instantly. Best-effort and idempotent; safe to await on the
1111
+ * hot path (cached after the first success). Requires hq to expose `memberId`
1112
+ * on the profile DTO (org-api whoami/members).
1113
+ */
1114
+ export async function resolveSelfMemberId(deps = {}) {
1115
+ if (_selfMemberId) return _selfMemberId;
1116
+ const fromDisk = persistedSelfMemberId();
1117
+ if (fromDisk) { _selfMemberId = fromDisk; return _selfMemberId; }
1118
+ try {
1119
+ const cfg = daemonOrgCfg();
1120
+ const conn = configFromAgent(cfg) || {};
1121
+ // configFromAgent returns the org key as `token` (NOT `apiKey`), and the
1122
+ // daemon's creds come from config/org.yaml, not process.env — so read `token`
1123
+ // first and only fall back to env. The slug lives at cfg.org.cohort.agentId
1124
+ // (same source ladderMemberId used); without it fetchSelfProfile can't hit
1125
+ // /members/<slug> and whoami is ambiguous in a multi-agent org.
1126
+ const apiKey = conn.token || conn.apiKey || process.env.COHORT_API_KEY || "";
1127
+ const orgId = conn.orgId || process.env.COHORT_ORG_ID || "";
1128
+ const agentId =
1129
+ conn.agentId ||
1130
+ (cfg && cfg.org && cfg.org.cohort && cfg.org.cohort.agentId) ||
1131
+ process.env.COHORT_AGENT_ID ||
1132
+ "";
1133
+ const base = conn.base || process.env.COHORT_BASE || "";
1134
+ if (!apiKey || !orgId) return _selfMemberId;
1135
+ const fetchSelf = deps.fetchSelfProfileImpl || fetchSelfProfile;
1136
+ const res = await fetchSelf({ base, apiKey, orgId, agentId });
1137
+ // fetchSelfProfile returns a res frame with the member under `result` (the
1138
+ // authed call() shape); tolerate `member` too for the raw directory route.
1139
+ const m = res && res.ok ? (res.result || res.member) : null;
1140
+ const mid = m && m.memberId ? String(m.memberId) : "";
1141
+ if (mid) {
1142
+ _selfMemberId = mid;
1143
+ try {
1144
+ const cfgPath = join(AGENT_REPO_DIR, "config", "agent.json");
1145
+ const c = JSON.parse(readFileSync(cfgPath, "utf-8"));
1146
+ c.memberId = mid;
1147
+ writeFileSync(cfgPath, JSON.stringify(c, null, 2) + "\n");
1148
+ } catch { /* best-effort persist — resolution still cached in-memory */ }
1149
+ }
1150
+ } catch { /* best-effort — falls back to slug until hq exposes memberId */ }
1151
+ return _selfMemberId;
1152
+ }
1153
+
1154
+ /** For tests: reset the cached self member id. */
1155
+ export function _resetSelfMemberId() { _selfMemberId = null; }
1156
+
1088
1157
  /** The seat's org member id — the `me` every directedness join is relative to. */
1089
1158
  function ladderMemberId() {
1159
+ // Prefer the resolved member cuid (the actor on events). Fall back to the slug
1160
+ // only until resolution completes — the slug can't power own-echo, but it is no
1161
+ // worse than the historical behaviour.
1162
+ if (_selfMemberId) return _selfMemberId;
1163
+ const disk = persistedSelfMemberId();
1164
+ if (disk) { _selfMemberId = disk; return disk; }
1090
1165
  const cfg = daemonOrgCfg();
1091
1166
  return (
1092
1167
  process.env.COHORT_AGENT_ID ||
@@ -1315,6 +1390,10 @@ export async function runExecutionLadder(item, service, itemId, trace_id, deps =
1315
1390
 
1316
1391
  let answered = null;
1317
1392
  try {
1393
+ // Ensure the seat's own member cuid is resolved BEFORE directedness runs, so
1394
+ // the own-echo guard can recognize this seat's own messages (the actor on an
1395
+ // event is a cuid, not the slug). Cached after the first resolve.
1396
+ await resolveSelfMemberId(deps);
1318
1397
  const me = ladderMemberId();
1319
1398
  const obligations = await ladderObligations(log);
1320
1399
 
@@ -830,3 +830,35 @@ test("NO FALLTHROUGH: a TRANSIENT quick-reply failure still falls through to a f
830
830
  assert.equal(dispatched, true, "a transient failure falls through to the full session, as before");
831
831
  assert.equal(res.path, "session");
832
832
  });
833
+
834
+ test("resolveSelfMemberId reads the member cuid from the res frame's `result` (own-echo keystone)", async () => {
835
+ daemon._resetSelfMemberId();
836
+ const pk = process.env.COHORT_API_KEY, po = process.env.COHORT_ORG_ID, pa = process.env.COHORT_AGENT_ID;
837
+ process.env.COHORT_API_KEY = "nlk_test";
838
+ process.env.COHORT_ORG_ID = "org_test";
839
+ process.env.COHORT_AGENT_ID = "A016";
840
+ try {
841
+ let calls = 0;
842
+ // fetchSelfProfile returns the authed call() res frame: the member is under
843
+ // `result`, and the org key is passed as apiKey. (Both were wrong in 2.11.7.)
844
+ const mid = await daemon.resolveSelfMemberId({
845
+ fetchSelfProfileImpl: async (o) => {
846
+ calls++;
847
+ assert.equal(o.agentId, "A016", "must pass the slug so /members/<slug> resolves");
848
+ return { ok: true, result: { memberId: "cmSELFcuid", slug: o.agentId } };
849
+ },
850
+ });
851
+ assert.equal(mid, "cmSELFcuid");
852
+ // cached: a second call must NOT refetch
853
+ const again = await daemon.resolveSelfMemberId({
854
+ fetchSelfProfileImpl: async () => { throw new Error("must not refetch a cached self id"); },
855
+ });
856
+ assert.equal(again, "cmSELFcuid");
857
+ assert.equal(calls, 1);
858
+ } finally {
859
+ daemon._resetSelfMemberId();
860
+ if (pk === undefined) delete process.env.COHORT_API_KEY; else process.env.COHORT_API_KEY = pk;
861
+ if (po === undefined) delete process.env.COHORT_ORG_ID; else process.env.COHORT_ORG_ID = po;
862
+ if (pa === undefined) delete process.env.COHORT_AGENT_ID; else process.env.COHORT_AGENT_ID = pa;
863
+ }
864
+ });