larkway 0.3.31 → 0.3.32

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.
Files changed (2) hide show
  1. package/dist/main.js +136 -4
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -118306,6 +118306,84 @@ function renderPrompt(input) {
118306
118306
  ].join("\n");
118307
118307
  }
118308
118308
 
118309
+ // src/lark/rosterResolver.ts
118310
+ import { execFile as execFile2 } from "node:child_process";
118311
+ function parseBotRoster(stdout) {
118312
+ const roster = /* @__PURE__ */ new Map();
118313
+ let json2;
118314
+ try {
118315
+ json2 = JSON.parse(stdout);
118316
+ } catch {
118317
+ return roster;
118318
+ }
118319
+ const items = json2?.data?.items;
118320
+ if (!Array.isArray(items)) return roster;
118321
+ for (const item of items) {
118322
+ if (typeof item !== "object" || item === null) continue;
118323
+ const record = item;
118324
+ const name = record["bot_name"];
118325
+ const id = record["bot_id"];
118326
+ if (typeof name === "string" && name && typeof id === "string" && id) {
118327
+ roster.set(name, id);
118328
+ }
118329
+ }
118330
+ return roster;
118331
+ }
118332
+ function remapPeersToLiveRoster(peers, liveRoster) {
118333
+ const remapped = [];
118334
+ const unresolved = [];
118335
+ const out = peers.map((peer) => {
118336
+ const liveId = liveRoster.get(peer.name);
118337
+ if (!liveId) {
118338
+ unresolved.push(peer.name);
118339
+ return peer;
118340
+ }
118341
+ if (liveId !== peer.id) {
118342
+ remapped.push(peer.name);
118343
+ return { ...peer, id: liveId };
118344
+ }
118345
+ return peer;
118346
+ });
118347
+ return { peers: out, remapped, unresolved };
118348
+ }
118349
+ async function resolveChatBotRoster(chatId, opts = {}) {
118350
+ const cli = opts.larkCliPath ?? "lark-cli";
118351
+ const args = ["im", "chat.members", "bots", "--chat-id", chatId, "--as", "bot"];
118352
+ if (opts.profile) args.push("--profile", opts.profile);
118353
+ const exec = opts.exec ?? defaultExec(opts.timeoutMs ?? 1e4);
118354
+ try {
118355
+ const stdout = await exec(cli, args);
118356
+ const roster = parseBotRoster(stdout);
118357
+ return roster.size > 0 ? roster : null;
118358
+ } catch {
118359
+ return null;
118360
+ }
118361
+ }
118362
+ function defaultExec(timeoutMs) {
118363
+ return (cmd, args) => new Promise((resolve2, reject) => {
118364
+ execFile2(cmd, args, { timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (err, stdout) => {
118365
+ if (err) reject(err);
118366
+ else resolve2(stdout);
118367
+ });
118368
+ });
118369
+ }
118370
+ function createCachedRosterResolver(opts) {
118371
+ const ttlMs = opts.ttlMs ?? 5 * 60 * 1e3;
118372
+ const now = opts.now ?? (() => Date.now());
118373
+ const cache = /* @__PURE__ */ new Map();
118374
+ return async (chatId) => {
118375
+ const hit = cache.get(chatId);
118376
+ if (hit && now() - hit.at < ttlMs) return hit.roster;
118377
+ const roster = await resolveChatBotRoster(chatId, {
118378
+ profile: opts.profile,
118379
+ larkCliPath: opts.larkCliPath,
118380
+ exec: opts.exec
118381
+ });
118382
+ cache.set(chatId, { roster, at: now() });
118383
+ return roster;
118384
+ };
118385
+ }
118386
+
118309
118387
  // src/agent/runner.ts
118310
118388
  var _registry = /* @__PURE__ */ new Map();
118311
118389
  function registerRunner(backend, factory) {
@@ -120132,7 +120210,7 @@ async function ensureNodeModules(worktreePath) {
120132
120210
  if (await pathExists(modulesMarker)) return;
120133
120211
  const start = Date.now();
120134
120212
  try {
120135
- await execFile2(
120213
+ await execFile3(
120136
120214
  "pnpm",
120137
120215
  ["install", "--offline", "--frozen-lockfile"],
120138
120216
  { cwd: monorepDir, timeoutMs: 18e4 }
@@ -120146,7 +120224,7 @@ async function ensureNodeModules(worktreePath) {
120146
120224
  `[bridge.handler] --offline install failed (will fall back to network): ${offlineErr.message.slice(0, 200)}`
120147
120225
  );
120148
120226
  }
120149
- await execFile2(
120227
+ await execFile3(
120150
120228
  "pnpm",
120151
120229
  ["install", "--frozen-lockfile"],
120152
120230
  { cwd: monorepDir, timeoutMs: 6e5 }
@@ -120155,7 +120233,7 @@ async function ensureNodeModules(worktreePath) {
120155
120233
  `[bridge.handler] pnpm install (network) ok (${((Date.now() - start) / 1e3).toFixed(1)}s) in ${monorepDir}`
120156
120234
  );
120157
120235
  }
120158
- function execFile2(cmd, args, opts) {
120236
+ function execFile3(cmd, args, opts) {
120159
120237
  return new Promise((resolve2, reject) => {
120160
120238
  const child = child_process.spawn(cmd, args, {
120161
120239
  cwd: opts.cwd,
@@ -120223,9 +120301,30 @@ async function createOnlyPostFallback(opts) {
120223
120301
  var BridgeHandler = class {
120224
120302
  deps;
120225
120303
  closed = false;
120304
+ /**
120305
+ * In-flight per-turn completion promises. run() dispatches handleOne
120306
+ * fire-and-forget (thread-serialized), so run() returning does NOT mean the
120307
+ * turn finished. Each entry resolves only when its handleOne AND all trailing
120308
+ * work (terminal render + ledger writes, incl. the failure-path fallback that
120309
+ * runs after the early settle) have completed. {@link whenAllTurnsSettled}
120310
+ * awaits these — the real turn boundary tests must sync assertions/cleanup on.
120311
+ */
120312
+ inFlightTurns = /* @__PURE__ */ new Set();
120226
120313
  constructor(deps) {
120227
120314
  this.deps = deps;
120228
120315
  }
120316
+ /**
120317
+ * Resolves when every dispatched turn has fully completed (handleOne returned
120318
+ * and its trailing render/ledger I/O drained). Test-facing sync point; a bridge
120319
+ * that keeps receiving events would keep finding work, so callers use this
120320
+ * after the event source is exhausted (e.g. right after `await run()` in a
120321
+ * single-message test).
120322
+ */
120323
+ async whenAllTurnsSettled() {
120324
+ while (this.inFlightTurns.size > 0) {
120325
+ await Promise.all([...this.inFlightTurns]);
120326
+ }
120327
+ }
120229
120328
  runtimeWarnings() {
120230
120329
  return (this.deps.runtimeRequirements ?? []).filter(
120231
120330
  (req) => !req.ok && (req.severity === "required" || req.kind === "secret")
@@ -120276,7 +120375,9 @@ var BridgeHandler = class {
120276
120375
  }).finally(() => {
120277
120376
  release();
120278
120377
  if (threadQueues.get(key) === next) threadQueues.delete(key);
120378
+ this.inFlightTurns.delete(next);
120279
120379
  });
120380
+ this.inFlightTurns.add(next);
120280
120381
  threadQueues.set(key, next);
120281
120382
  }
120282
120383
  }
@@ -120659,6 +120760,34 @@ var BridgeHandler = class {
120659
120760
  while (true) {
120660
120761
  attempt++;
120661
120762
  const currentIsNewThread = currentExisting === void 0;
120763
+ let effectivePeers = this.deps.peers;
120764
+ if (effectivePeers?.length && this.deps.resolveLiveRoster) {
120765
+ try {
120766
+ const liveRoster = await this.deps.resolveLiveRoster(parsed.chatId);
120767
+ if (liveRoster) {
120768
+ const { peers: remappedPeers, remapped, unresolved } = remapPeersToLiveRoster(
120769
+ effectivePeers,
120770
+ liveRoster
120771
+ );
120772
+ effectivePeers = remappedPeers;
120773
+ if (remapped.length > 0 || unresolved.length > 0) {
120774
+ await recordEvent({
120775
+ status: "running",
120776
+ appendPath: "peer roster",
120777
+ reason: `live-roster resolve: remapped [${remapped.join(", ") || "none"}] to same-app-scope open_id; unresolved (kept static config id, may not be deliverable) [${unresolved.join(", ") || "none"}].`
120778
+ });
120779
+ }
120780
+ } else {
120781
+ await recordEvent({
120782
+ status: "running",
120783
+ appendPath: "peer roster",
120784
+ reason: "live-roster resolve returned nothing; kept static <peer-bots> open_ids (may be cross-app-scope / undeliverable)."
120785
+ });
120786
+ }
120787
+ } catch (err) {
120788
+ console.warn("[bridge.handler] live-roster resolve failed (using static peers):", err);
120789
+ }
120790
+ }
120662
120791
  const prompt = renderPrompt({
120663
120792
  parsed,
120664
120793
  isNewThread: currentIsNewThread,
@@ -120680,7 +120809,7 @@ var BridgeHandler = class {
120680
120809
  readOnly: conventions.readOnly,
120681
120810
  gitlabTokenEnvName: conventions.gitlabTokenEnvName
120682
120811
  },
120683
- peers: this.deps.peers,
120812
+ peers: effectivePeers,
120684
120813
  turn_taking_limit: this.deps.botConfig?.turn_taking_limit,
120685
120814
  botName: this.deps.botConfig?.name,
120686
120815
  backend: this.deps.botConfig?.backend,
@@ -126297,6 +126426,9 @@ async function runV2Mode({
126297
126426
  gitlabToken: effectiveGitlabToken,
126298
126427
  agentMemory: bot.agent_memory,
126299
126428
  larkCliProfile,
126429
+ // PRB-6/§11.3: resolve peer @ targets to same-app-scope open_ids from the
126430
+ // live chat roster (per-chat cached), only when this bot actually has peers.
126431
+ resolveLiveRoster: resolvedPeers.length > 0 ? createCachedRosterResolver({ profile: larkCliProfile }) : void 0,
126300
126432
  runtimeRequirements: runtimeRequirementsForBots([bot]),
126301
126433
  recordRuntimeEvent: async (patch) => {
126302
126434
  await upsertRuntimeEvent(larkwayHome(), bot.id, patch);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkway",
3
- "version": "0.3.31",
3
+ "version": "0.3.32",
4
4
  "description": "Thin bridge: Feishu thread to local Claude Code CLI",
5
5
  "license": "MIT",
6
6
  "author": "Chuck Wu (chuckwu0)",