@corenel/sidecar 0.8.8 → 0.10.0

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/cli.js +525 -79
  2. package/package.json +6 -6
package/dist/cli.js CHANGED
@@ -17291,8 +17291,8 @@ ${yamlContent}` : yamlContent;
17291
17291
  processedContent = content;
17292
17292
  }
17293
17293
  if (this.verbose) {
17294
- const preview = processedContent.substring(0, 50).replace(/\n/g, "\\n");
17295
- console.log(`[include] Loaded ${ext} file: ${filePath} (${preview}...)`);
17294
+ const preview2 = processedContent.substring(0, 50).replace(/\n/g, "\\n");
17295
+ console.log(`[include] Loaded ${ext} file: ${filePath} (${preview2}...)`);
17296
17296
  }
17297
17297
  return {
17298
17298
  src: processedContent,
@@ -17914,8 +17914,8 @@ ${content}` : parentData.content;
17914
17914
  }
17915
17915
  removedCount++;
17916
17916
  if (verbose) {
17917
- const preview = blockContent.substring(0, 50).replace(/\n/g, "\\n");
17918
- console.log(` - Removing \`\`\`${lang} block: "${preview}..."`);
17917
+ const preview2 = blockContent.substring(0, 50).replace(/\n/g, "\\n");
17918
+ console.log(` - Removing \`\`\`${lang} block: "${preview2}..."`);
17919
17919
  }
17920
17920
  return "";
17921
17921
  });
@@ -18765,6 +18765,25 @@ ${firstUserMsg.content}`;
18765
18765
  }
18766
18766
  });
18767
18767
 
18768
+ // ../tools-node/toolGuidance.ts
18769
+ function nodeToolGuidance(toolNames) {
18770
+ const has3 = new Set(toolNames);
18771
+ const parts = [];
18772
+ if (has3.has(SHELL_TOOL)) parts.push(SHELL);
18773
+ if (PROC_TOOLS.some((t2) => has3.has(t2))) parts.push(PROCS);
18774
+ return parts.join("\n\n");
18775
+ }
18776
+ var SHELL_TOOL, PROC_TOOLS, SHELL, PROCS;
18777
+ var init_toolGuidance = __esm({
18778
+ "../tools-node/toolGuidance.ts"() {
18779
+ "use strict";
18780
+ SHELL_TOOL = "shell";
18781
+ PROC_TOOLS = ["proc_start", "proc_stop", "proc_list"];
18782
+ SHELL = `Running a command is often the shortest correct answer: use \`${SHELL_TOOL}\` for anything a command already does well -- listing, searching, git, package managers, build and test runners -- instead of reconstructing it from file reads. It waits for the command to finish and returns its output, so it is the wrong tool for anything that does not exit on its own.`;
18783
+ PROCS = `A command that keeps running (a dev server, a watcher, a queue worker) belongs to \`${PROC_TOOLS[0]}\`, not \`${SHELL_TOOL}\` -- starting one with \`${SHELL_TOOL}\` hangs the turn until it is killed. Use \`${PROC_TOOLS[2]}\` to see what is already running before starting anything, and \`${PROC_TOOLS[1]}\` to stop what you started. Do not stop a process you did not start.`;
18784
+ }
18785
+ });
18786
+
18768
18787
  // ../tools-node/promptHost.ts
18769
18788
  function backend(svc) {
18770
18789
  return {
@@ -18822,8 +18841,22 @@ function nodePromptHost(workspace, project) {
18822
18841
  sources[p2] = liveSrc;
18823
18842
  return compile("", values, new HybridFileSystem(sources, backend(workspace)), "/" + p2);
18824
18843
  },
18825
- siteIndex: () => ""
18844
+ siteIndex: () => "",
18826
18845
  // no site index in node (the help chat is browser-only)
18846
+ /* Who the agent says it IS. Without this, `composeAgentSystem` falls back
18847
+ * to its `'Prompd'` default and the `corenel` binary's agent introduced
18848
+ * itself as "the Prompd assistant" -- the same leak already fixed for the
18849
+ * browser help chat (composeHelpSystem) and missed here, because the node
18850
+ * host is the one PromptHost that never implemented the hook.
18851
+ *
18852
+ * Constant, not a parameter: every node consumer of this host (the CLI and
18853
+ * the sidecar) IS Corenel. A host that is not should implement its own. */
18854
+ appName: () => "Corenel",
18855
+ /* The product's guidance for the tools this run actually has -- the shell,
18856
+ * long-running processes, and deferred MCP tools. Gated per tool inside
18857
+ * nodeToolGuidance, because this same host serves the sidecar, whose shell
18858
+ * is behind --allow-shell. */
18859
+ toolGuidance: (names) => nodeToolGuidance(names)
18827
18860
  };
18828
18861
  }
18829
18862
  var parser, compiler;
@@ -18832,6 +18865,7 @@ var init_promptHost = __esm({
18832
18865
  "use strict";
18833
18866
  init_dist();
18834
18867
  init_protocol();
18868
+ init_toolGuidance();
18835
18869
  parser = new PrompdParser();
18836
18870
  compiler = new PrompdCompiler();
18837
18871
  }
@@ -25434,6 +25468,11 @@ var SidecarServer = class {
25434
25468
  /** The friendly label from the same `hello`, kept beside `clientId` so the
25435
25469
  * dirName resolver is handed the identity the manifest keys on. */
25436
25470
  clientName;
25471
+ /** This connection's attended-client object, retained so an answer it sends
25472
+ * can be attributed to it. Typed `unknown` because its only consumer is the
25473
+ * registry's own identity lookup, which takes `unknown` for the
25474
+ * dependency-direction reason on `SidecarAttendanceRegistry.answer`. */
25475
+ attended;
25437
25476
  /** Detach function for this connection's attendance attachment, set on a
25438
25477
  * successful `hello` when `opts.attendance` is wired. Called once, on channel
25439
25478
  * close, so a dead connection stops receiving/counting toward asks. */
@@ -25545,7 +25584,7 @@ var SidecarServer = class {
25545
25584
  return;
25546
25585
  case "permission-answer":
25547
25586
  if (!this.authed) return;
25548
- this.opts.attendance?.answer(m2.id, m2.decision);
25587
+ this.opts.attendance?.answer(m2.id, m2.decision, this.attended);
25549
25588
  return;
25550
25589
  case "roots-browse":
25551
25590
  if (!this.authed) {
@@ -25795,10 +25834,11 @@ var SidecarServer = class {
25795
25834
  }
25796
25835
  this.authed = true;
25797
25836
  this.hostStateDir = m2.stateDir ?? ".corenel";
25837
+ let accounts = [];
25798
25838
  if (typeof m2.clientId === "string" && m2.clientId) {
25799
25839
  const wireId = m2.clientId.slice(0, MAX_IDENTITY_FIELD_LEN);
25800
25840
  const claimed = Array.isArray(m2.accounts) ? m2.accounts : [];
25801
- const accounts = claimed.length > MAX_CLAIMED_ACCOUNTS ? [] : claimed.filter((a2) => typeof a2 === "string" && a2.length <= MAX_IDENTITY_FIELD_LEN);
25841
+ accounts = claimed.length > MAX_CLAIMED_ACCOUNTS ? [] : claimed.filter((a2) => typeof a2 === "string" && a2.length <= MAX_IDENTITY_FIELD_LEN);
25802
25842
  let clientId = wireId;
25803
25843
  if (this.opts.clientKey) {
25804
25844
  try {
@@ -25827,11 +25867,16 @@ var SidecarServer = class {
25827
25867
  }
25828
25868
  }
25829
25869
  if (this.opts.attendance) {
25830
- this.attendedDetach = this.opts.attendance.attach({
25870
+ const attended = {
25831
25871
  send: (ask) => this.ch.send({ t: "permission-ask", ...ask }),
25832
- resolved: (id, decision, by) => this.ch.send({ t: "permission-resolved", id, decision, by }),
25872
+ resolved: (id, decision, by, byClientId) => this.ch.send({ t: "permission-resolved", id, decision, by, ...byClientId ? { byClientId } : {} }),
25833
25873
  runActivity: (activity) => this.ch.send({ t: "run-activity", activity }),
25834
25874
  registrations: (registrations) => this.ch.send({ t: "registrations", registrations })
25875
+ };
25876
+ this.attended = attended;
25877
+ this.attendedDetach = this.opts.attendance.attach(attended, {
25878
+ clientId: this.clientId ?? "",
25879
+ accounts
25835
25880
  });
25836
25881
  }
25837
25882
  this.opts.log?.(`paired \u2014 ${this.tools.size} ${this.tools.size === 1 ? "tool" : "tools"} advertised (host dir: ${this.hostStateDir})`);
@@ -26509,8 +26554,20 @@ var SidecarServer = class {
26509
26554
  this.ch.send({ t: "room-open-result", id: m2.id, ok: false, problems: ["this sidecar does not host rooms"] });
26510
26555
  return;
26511
26556
  }
26557
+ if (this.opts.teamMode && this.clientId === void 0) {
26558
+ this.ch.send({
26559
+ t: "room-open-result",
26560
+ id: m2.id,
26561
+ ok: false,
26562
+ problems: ["This connection has no verified identity (no clientId from hello), so it cannot open a room."]
26563
+ });
26564
+ return;
26565
+ }
26566
+ const clientId = this.clientId;
26567
+ const owner = clientId === void 0 ? void 0 : m2.req.participants.find((p2) => p2.kind === "human" && p2.role === "owner");
26568
+ const req = owner && clientId !== void 0 ? { ...m2.req, participants: m2.req.participants.map((p2) => p2 === owner ? { ...p2, id: clientId } : p2) } : m2.req;
26512
26569
  try {
26513
- const r2 = await this.opts.rooms.open(m2.req);
26570
+ const r2 = await this.opts.rooms.open(req);
26514
26571
  this.ch.send(r2.ok ? { t: "room-open-result", id: m2.id, ok: true, room: r2.room } : { t: "room-open-result", id: m2.id, ok: false, problems: r2.problems });
26515
26572
  } catch (err) {
26516
26573
  this.ch.send({ t: "room-open-result", id: m2.id, ok: false, problems: [errMessage(err)] });
@@ -26522,7 +26579,9 @@ var SidecarServer = class {
26522
26579
  return;
26523
26580
  }
26524
26581
  try {
26525
- this.ch.send({ t: "room-list-result", id: m2.id, ok: true, rooms: await this.opts.rooms.list() });
26582
+ const rooms = await this.opts.rooms.list();
26583
+ const mine = !this.opts.teamMode ? rooms : this.clientId === void 0 ? [] : rooms.filter((r2) => r2.participants.some((p2) => p2.id === this.clientId));
26584
+ this.ch.send({ t: "room-list-result", id: m2.id, ok: true, rooms: mine });
26526
26585
  } catch (err) {
26527
26586
  this.ch.send({ t: "room-list-result", id: m2.id, ok: false, error: errMessage(err) });
26528
26587
  }
@@ -26534,6 +26593,26 @@ var SidecarServer = class {
26534
26593
  }
26535
26594
  try {
26536
26595
  const found = await this.opts.rooms.read(m2.roomId);
26596
+ if (found && this.opts.teamMode) {
26597
+ if (this.clientId === void 0) {
26598
+ this.ch.send({
26599
+ t: "room-read-result",
26600
+ id: m2.id,
26601
+ ok: false,
26602
+ error: "This connection has no verified identity, so membership in this room cannot be proven."
26603
+ });
26604
+ return;
26605
+ }
26606
+ if (!found.room.participants.some((p2) => p2.id === this.clientId)) {
26607
+ this.ch.send({
26608
+ t: "room-read-result",
26609
+ id: m2.id,
26610
+ ok: false,
26611
+ error: `"${this.clientId}" is not a member of room "${m2.roomId}".`
26612
+ });
26613
+ return;
26614
+ }
26615
+ }
26537
26616
  this.ch.send(found ? { t: "room-read-result", id: m2.id, ok: true, room: found.room, messages: found.messages, turnCosts: found.turnCosts } : { t: "room-read-result", id: m2.id, ok: true, room: null, messages: [], turnCosts: [] });
26538
26617
  } catch (err) {
26539
26618
  this.ch.send({ t: "room-read-result", id: m2.id, ok: false, error: errMessage(err) });
@@ -26551,13 +26630,45 @@ var SidecarServer = class {
26551
26630
  this.ch.send({ t: "room-resume-result", id: m2.id, ok: false, error: errMessage(err) });
26552
26631
  }
26553
26632
  }
26633
+ /* ONE identity rule for every room verb.
26634
+ *
26635
+ * `room-read` judges membership on `this.clientId` -- the identity this
26636
+ * connection PROVED at `hello`. The mutating verbs used to hand
26637
+ * `m.participantId` straight off the wire to `requireRole`/`mayAct`, so one
26638
+ * branch enforced while another trusted the caller: a viewer could send the
26639
+ * owner's participantId and close the room. Under team mode every verb now
26640
+ * judges the caller the same way `room-read` does, and a connection with no
26641
+ * proven identity is refused rather than attributed to whatever it claimed.
26642
+ *
26643
+ * KNOWN, DELIBERATE CONSEQUENCE (R9): the observed `clientId` and the
26644
+ * participant ids a room is opened with are currently DIFFERENT NAMESPACES
26645
+ * for everyone but the owner (whose id `onRoomOpen` overwrites with the
26646
+ * observed one). Until those namespaces are reconciled, these verbs will
26647
+ * REFUSE non-owner participants under `--mode team`. That refusal is the
26648
+ * intended outcome -- fail closed, consistently -- and must NOT be "fixed"
26649
+ * by falling back to the wire id, which is the hole this closes.
26650
+ *
26651
+ * With `--mode team` OFF this is the pre-existing behaviour, untouched: the
26652
+ * wire id is attribution only, exactly as the spec knowingly accepted. */
26653
+ actingId(claimed) {
26654
+ if (!this.opts.teamMode) return { ok: true, id: claimed };
26655
+ if (this.clientId === void 0) {
26656
+ return { ok: false, error: "This connection has no verified identity (no clientId from hello), so it cannot act on a room." };
26657
+ }
26658
+ return { ok: true, id: this.clientId };
26659
+ }
26554
26660
  async onRoomAnswer(m2) {
26555
26661
  if (!this.opts.rooms) {
26556
26662
  this.ch.send({ t: "room-answer-result", id: m2.id, ok: false, error: "this sidecar does not host rooms" });
26557
26663
  return;
26558
26664
  }
26559
26665
  try {
26560
- const room = await this.opts.rooms.answer(m2.roomId, m2.participantId, m2.text, m2.addressed);
26666
+ const actor = this.actingId(m2.participantId);
26667
+ if (!actor.ok) {
26668
+ this.ch.send({ t: "room-answer-result", id: m2.id, ok: false, error: actor.error });
26669
+ return;
26670
+ }
26671
+ const room = await this.opts.rooms.answer(m2.roomId, actor.id, m2.text, m2.addressed);
26561
26672
  this.ch.send({ t: "room-answer-result", id: m2.id, ok: true, room });
26562
26673
  } catch (err) {
26563
26674
  this.ch.send({ t: "room-answer-result", id: m2.id, ok: false, error: errMessage(err) });
@@ -26569,7 +26680,12 @@ var SidecarServer = class {
26569
26680
  return;
26570
26681
  }
26571
26682
  try {
26572
- const room = await this.opts.rooms.interject(m2.roomId, m2.participantId, m2.text);
26683
+ const actor = this.actingId(m2.participantId);
26684
+ if (!actor.ok) {
26685
+ this.ch.send({ t: "room-interject-result", id: m2.id, ok: false, error: actor.error });
26686
+ return;
26687
+ }
26688
+ const room = await this.opts.rooms.interject(m2.roomId, actor.id, m2.text);
26573
26689
  this.ch.send({ t: "room-interject-result", id: m2.id, ok: true, room });
26574
26690
  } catch (err) {
26575
26691
  this.ch.send({ t: "room-interject-result", id: m2.id, ok: false, error: errMessage(err) });
@@ -26581,7 +26697,12 @@ var SidecarServer = class {
26581
26697
  return;
26582
26698
  }
26583
26699
  try {
26584
- const room = await this.opts.rooms.close(m2.roomId, m2.participantId, m2.reason);
26700
+ const actor = this.actingId(m2.participantId);
26701
+ if (!actor.ok) {
26702
+ this.ch.send({ t: "room-close-result", id: m2.id, ok: false, error: actor.error });
26703
+ return;
26704
+ }
26705
+ const room = await this.opts.rooms.close(m2.roomId, actor.id, m2.reason);
26585
26706
  this.ch.send({ t: "room-close-result", id: m2.id, ok: true, room });
26586
26707
  } catch (err) {
26587
26708
  this.ch.send({ t: "room-close-result", id: m2.id, ok: false, error: errMessage(err) });
@@ -26680,7 +26801,12 @@ var SidecarServer = class {
26680
26801
  return;
26681
26802
  }
26682
26803
  try {
26683
- const findings = await this.opts.rooms.editFinding(m2.roomId, m2.participantId, m2.edit);
26804
+ const actor = this.actingId(m2.participantId);
26805
+ if (!actor.ok) {
26806
+ this.ch.send({ t: "room-finding-edit-result", id: m2.id, ok: false, error: actor.error });
26807
+ return;
26808
+ }
26809
+ const findings = await this.opts.rooms.editFinding(m2.roomId, actor.id, m2.edit);
26684
26810
  this.ch.send({ t: "room-finding-edit-result", id: m2.id, ok: true, findings });
26685
26811
  } catch (err) {
26686
26812
  this.ch.send({ t: "room-finding-edit-result", id: m2.id, ok: false, error: errMessage(err) });
@@ -26983,6 +27109,7 @@ function startSidecar(opts) {
26983
27109
  procs: opts.procs,
26984
27110
  sessions: opts.sessions,
26985
27111
  rooms: opts.rooms,
27112
+ teamMode: opts.teamMode,
26986
27113
  pauses: opts.pauses,
26987
27114
  clientDirName: opts.clientDirName,
26988
27115
  hosts: opts.hosts,
@@ -27167,6 +27294,7 @@ var FLAGS = [
27167
27294
  { flag: "--allow-origin", arg: "<url>", help: "Allow a browser origin to connect. Repeatable." },
27168
27295
  { flag: "--allow-shell", help: "Enable the shell tool. Off by default." },
27169
27296
  { flag: "--allow-lan", help: "Serve the local network, not just this machine: binds all interfaces and serves wss with a self-signed cert. --host / --tls-cert override each half." },
27297
+ { flag: "--mode", arg: "team", help: "Route a permission ask to the connection whose run raised it, plus the other devices that owner is signed in on, rather than to everyone attached. The identity a client sends is CLAIMED and this daemon cannot verify it: attribution, not authorization. An ask is never diverted away from its owner, so a false claim can join a prompt but never steal one." },
27170
27298
  { flag: "--tls-cert", arg: "<file>", help: "TLS certificate for wss://." },
27171
27299
  { flag: "--tls-key", arg: "<file>", help: "TLS private key for wss://." },
27172
27300
  { flag: "--self-signed", help: "Generate a self-signed certificate instead of supplying one." },
@@ -28130,6 +28258,7 @@ var KNOWN_KEYS = [
28130
28258
  "maxChainDepth",
28131
28259
  "name",
28132
28260
  "appUrl",
28261
+ "mode",
28133
28262
  "defaultModel",
28134
28263
  "mcpServers",
28135
28264
  "mcpAllow"
@@ -29290,7 +29419,11 @@ var STANDARD_POLICY = {
29290
29419
  name: "standard",
29291
29420
  description: "Read-only tools run automatically; writes ask for approval.",
29292
29421
  onViolation: "stop",
29293
- maxTurns: 8,
29422
+ // maxTurns counts iterations WITHIN one user message (one model call plus
29423
+ // its tool round-trip), not messages across a conversation -- 8 was
29424
+ // exhausted inside a single tool-heavy reply. 0 means unlimited (see
29425
+ // core/loop.ts); this default stays a finite backstop.
29426
+ maxTurns: 40,
29294
29427
  permissions: {
29295
29428
  "prompd:propose_edit": "allow",
29296
29429
  // proposals are reviewed in the diff UI, not applied directly
@@ -29304,7 +29437,8 @@ var READ_ONLY_POLICY = {
29304
29437
  name: "read-only",
29305
29438
  description: "Inspection only \u2014 all writes and side effects are refused.",
29306
29439
  onViolation: "stop",
29307
- maxTurns: 8,
29440
+ maxTurns: 40,
29441
+ // see STANDARD_POLICY: counts iterations within one reply, not conversation turns
29308
29442
  tools: {
29309
29443
  deny: [
29310
29444
  "fs:write_file",
@@ -29347,7 +29481,9 @@ var AUTONOMOUS_POLICY = {
29347
29481
  name: "autonomous",
29348
29482
  description: "Everything runs automatically \u2014 no approval prompts (system config still asks).",
29349
29483
  onViolation: "warn",
29350
- maxTurns: 16,
29484
+ // Higher than the other builtins: nobody is watching an autonomous run turn
29485
+ // by turn, so it gets more headroom before the backstop kicks in.
29486
+ maxTurns: 80,
29351
29487
  permissions: {
29352
29488
  "system:*": "ask"
29353
29489
  // even unattended, never silently reconfigure the harness
@@ -30080,6 +30216,10 @@ var forgetMemoryTool = {
30080
30216
  return `Forgot memory "${name}".`;
30081
30217
  }
30082
30218
  };
30219
+ function preview(v2, max = 200) {
30220
+ if (v2.length <= max) return JSON.stringify(v2);
30221
+ return `${JSON.stringify(v2.slice(0, max))}... (${v2.length} chars total)`;
30222
+ }
30083
30223
  var kvGetTool = {
30084
30224
  name: "kv_get",
30085
30225
  description: "Read a value you previously saved with kv_set, by key, from the local key/value store. This is a general note store \u2014 NOT a filesystem, working directory, or settings: use list_files/read_file for files.",
@@ -30099,10 +30239,42 @@ var kvSetTool = {
30099
30239
  required: ["key", "value"]
30100
30240
  },
30101
30241
  // Permission is gated centrally by the loop's policy/permission layer.
30242
+ /*
30243
+ * THE RESULT STATES WHAT WAS STORED, AND WHAT IT REPLACED.
30244
+ *
30245
+ * This used to return `Set ${key}.` -- nothing else. Two consequences, both
30246
+ * observed in a real run:
30247
+ *
30248
+ * 1. "set bread=true" and "set bread=false" produced BYTE-IDENTICAL tool
30249
+ * results. An agent had no feedback that could distinguish a correct
30250
+ * write from a wrong one, so a mistaken value was indistinguishable from
30251
+ * a correct one for the rest of the conversation.
30252
+ *
30253
+ * 2. A LOST UPDATE was invisible. The common shape here is a list kept as
30254
+ * one blob under one key, updated read-modify-write: a later write
30255
+ * carrying a stale copy silently reverts an earlier one, and every
30256
+ * result still says "Set". A run marked two items done and only one
30257
+ * stayed done, with nothing anywhere to show the second had been undone.
30258
+ *
30259
+ * So: report the value, and name the value being replaced. An agent that
30260
+ * overwrites "apples done, bread done" with "apples done, bread pending"
30261
+ * now reads back, in the tool result it already has to process, exactly
30262
+ * what it just destroyed. The read-back is what makes this a check rather
30263
+ * than an echo of the argument -- a store that transformed, truncated or
30264
+ * dropped the write is caught here instead of at the summary.
30265
+ */
30102
30266
  async run(args, ctx) {
30103
30267
  const key = str4(args, "key");
30104
- await ctx.kv.set(key, str4(args, "value"));
30105
- return `Set ${key}.`;
30268
+ const value = str4(args, "value");
30269
+ const before = await ctx.kv.get(key);
30270
+ await ctx.kv.set(key, value);
30271
+ const after = await ctx.kv.get(key);
30272
+ if (after !== value) {
30273
+ return `WRITE DID NOT TAKE. "${key}" was set to ${preview(value)} but reads back as ${after == null ? "(not set)" : preview(after)}. Do not rely on this key; do not report it as saved.`;
30274
+ }
30275
+ if (before == null) return `Set "${key}" = ${preview(value)} (new key).`;
30276
+ if (before === value) return `Set "${key}" = ${preview(value)} (unchanged).`;
30277
+ return `Set "${key}" = ${preview(value)}, REPLACING ${preview(before)}. If that previous value held anything you did not mean to discard, it is gone.`;
30106
30278
  }
30107
30279
  };
30108
30280
  var recallResultTool = {
@@ -30604,6 +30776,18 @@ parameters:
30604
30776
  type: string
30605
30777
  description: The editor file / selection / compiled-output context block.
30606
30778
  default: ""
30779
+ - name: skills
30780
+ type: string
30781
+ description: Instructions contributed by the skills enabled for this run.
30782
+ default: ""
30783
+ - name: memory
30784
+ type: string
30785
+ description: The index of notes saved to this run's memory plane.
30786
+ default: ""
30787
+ - name: run_context
30788
+ type: string
30789
+ description: Facts about THIS run the model cannot otherwise know \u2014 the date, and whatever the host adds (working directory, platform).
30790
+ default: ""
30607
30791
  - name: app
30608
30792
  type: string
30609
30793
  description: Product name of the host application, used for the agent's own identity.
@@ -30611,13 +30795,22 @@ parameters:
30611
30795
  ---
30612
30796
  {% if persona %}{{ persona }}
30613
30797
 
30614
- {% endif %}You are the {{ app }} assistant. You help the user do real work in {{ app }}: reading and changing files, composing and running prompts and workflows, and building software. What is in front of you differs by surface \u2014 an open file, a workflow canvas, a workspace, or nothing but this conversation \u2014 so work from the context you were actually given rather than assuming a particular screen.
30798
+ {% endif %}You are the {{ app }} assistant. You help the user do real work: reading and changing files, composing and running prompts and workflows, and building software. Work from the context you were given below rather than assuming a particular screen.
30615
30799
 
30616
- To read or change files in the workspace, use the file tools; writes ask the user for permission first. Use compile_prompd to preview rendered output and search_packages to find registry packages. For any multi-step job, maintain a visible task list with todo_write: lay out the steps, keep one task in_progress, and mark each completed as you go. Prefer calling a tool over guessing, and be concise.
30800
+ Verify before you claim. Do not say something is done, fixed, passing or ready unless you checked it in this conversation and can point at what you checked. If you did not run it, say you did not run it. If a check failed, say so plainly and show what failed. A wrong claim of success costs the user far more than the check would have.
30617
30801
 
30618
30802
  Say what you assumed and what you did not do. A request that leaves room for interpretation gets its decisions named, one line each, so the user can correct one without unpicking the rest. How much you decide for yourself versus ask about is the mode's call, below.
30619
30803
 
30620
- ## Tools available this run
30804
+ A tool call that comes back denied, blocked or unavailable is a decision, not a failure to route around. Do not retry it, do not reach for another tool to accomplish the same thing, and never describe the change as made. Say what you wanted to do and stop.
30805
+
30806
+ Prefer calling a tool over guessing: read the file rather than recalling it, run the command rather than predicting its output. Use the file tools to read and change files, and compile_prompd to preview rendered output. For a job with several steps, keep a visible task list with todo_write and mark each step as you finish it.
30807
+
30808
+ Be brief. Lead with the answer, then only what the user needs to act on or check it. Use headings, lists and tables where they carry something prose would not, and prose everywhere else. Do not restate the request back, do not narrate what you are about to do, and do not end by summarizing what the user has just read.
30809
+
30810
+ {% if run_context %}## This run
30811
+ {{ run_context }}
30812
+
30813
+ {% endif %}## Tools available this run
30621
30814
  {{ tools }}
30622
30815
  {% if tool_guidance %}
30623
30816
 
@@ -30628,6 +30821,14 @@ Say what you assumed and what you did not do. A request that leaves room for int
30628
30821
  ## Mode
30629
30822
  {{ mode_hint }}
30630
30823
  {% endif %}
30824
+ {% if skills %}
30825
+
30826
+ {{ skills }}
30827
+ {% endif %}
30828
+ {% if memory %}
30829
+
30830
+ {{ memory }}
30831
+ {% endif %}
30631
30832
  {% if file_context %}
30632
30833
 
30633
30834
  {{ file_context }}
@@ -31890,26 +32091,39 @@ function makeAgentMemoryProvider(getFiles, memoryRoot) {
31890
32091
  };
31891
32092
  }
31892
32093
 
32094
+ // ../crew/room/types.ts
32095
+ function roleOf(p2) {
32096
+ return p2.kind === "agent" ? null : p2.role ?? "contributor";
32097
+ }
32098
+
31893
32099
  // ../crew/room/order.ts
31894
32100
  var ID_RE = /^[A-Za-z0-9_-]+$/;
31895
32101
  function isValidParticipantId(id) {
31896
32102
  return ID_RE.test(id);
31897
32103
  }
32104
+ function rotating(state) {
32105
+ return state.mode === "conversation" ? state.participants.filter((p2) => p2.kind === "agent") : state.participants;
32106
+ }
31898
32107
  function currentSpeaker(state) {
31899
32108
  if (state.closed) return null;
31900
- if (state.jump) return state.participants.find((p2) => p2.id === state.jump) ?? null;
31901
- return state.participants[state.floor] ?? null;
32109
+ const roster = rotating(state);
32110
+ if (state.jump) {
32111
+ const jumped = roster.find((p2) => p2.id === state.jump);
32112
+ if (jumped) return jumped;
32113
+ }
32114
+ return roster[state.floor] ?? null;
31902
32115
  }
31903
32116
  function advance(state, addressed) {
31904
32117
  if (state.closed) return state;
32118
+ const roster = rotating(state);
31905
32119
  const speaker = currentSpeaker(state);
31906
32120
  const spokeViaJump = state.jump != null;
31907
32121
  const turns = state.turns + 1;
31908
32122
  const mayAddress = !spokeViaJump;
31909
- const target = mayAddress && addressed && addressed !== speaker?.id && state.participants.some((p2) => p2.id === addressed) ? addressed : null;
31910
- let floor = spokeViaJump ? state.floor : (state.floor + 1) % state.participants.length;
31911
- if (target != null && state.participants[floor]?.id === target) {
31912
- floor = (floor + 1) % state.participants.length;
32123
+ const target = mayAddress && addressed && addressed !== speaker?.id && roster.some((p2) => p2.id === addressed) ? addressed : null;
32124
+ let floor = spokeViaJump ? state.floor : (state.floor + 1) % roster.length;
32125
+ if (target != null && roster[floor]?.id === target) {
32126
+ floor = (floor + 1) % roster.length;
31913
32127
  }
31914
32128
  const closed = turns >= state.maxTurns ? { by: "room", reason: `Turn cap reached (${state.maxTurns}).`, at: (/* @__PURE__ */ new Date()).toISOString() } : null;
31915
32129
  return { ...state, floor, jump: target, jumpBy: target ? speaker?.id ?? null : null, turns, closed };
@@ -31957,6 +32171,9 @@ function foldRoom(text) {
31957
32171
  maxUsd: typeof line.maxUsd === "number" ? line.maxUsd : null,
31958
32172
  spentUsd: 0,
31959
32173
  closed: null,
32174
+ // Absent means a room written before conversation mode existed, and
32175
+ // every one of those is a rotation room.
32176
+ mode: line.mode ?? "rotation",
31960
32177
  // Absent, rather than 0, on a line whose `at` is missing or not a
31961
32178
  // number: "the log does not say" is not a date at the epoch.
31962
32179
  ...typeof line.at === "number" ? { openedAt: line.at } : {}
@@ -32036,6 +32253,7 @@ function foldSession(text) {
32036
32253
  const turns = [];
32037
32254
  let title = "";
32038
32255
  let titleLocked;
32256
+ let movedTo;
32039
32257
  let updatedAt = 0;
32040
32258
  for (const raw of text.split("\n")) {
32041
32259
  const s2 = raw.trim();
@@ -32057,6 +32275,9 @@ function foldSession(text) {
32057
32275
  } else if (line.k === "turn" && Number.isInteger(line.i) && line.i >= 0 && typeof line.at === "number") {
32058
32276
  turns[line.i] = line.m;
32059
32277
  updatedAt = Math.max(updatedAt, line.at);
32278
+ } else if (line.k === "moved" && typeof line.roomId === "string" && typeof line.at === "number") {
32279
+ movedTo = { movedToRoom: line.roomId, at: line.at };
32280
+ updatedAt = Math.max(updatedAt, line.at);
32060
32281
  }
32061
32282
  }
32062
32283
  if (!meta) return null;
@@ -32067,7 +32288,12 @@ function foldSession(text) {
32067
32288
  updatedAt: updatedAt || meta.createdAt,
32068
32289
  messages: turns.filter((t2) => t2 !== void 0),
32069
32290
  ...meta.surface ? { surface: meta.surface } : {},
32070
- ...titleLocked !== void 0 ? { titleLocked } : {}
32291
+ // Only a STRING owner survives the fold: the line came from disk (or a
32292
+ // sidecar), so its declared type guarantees nothing, and a non-string here
32293
+ // would reach a comparison that decides how a row is drawn.
32294
+ ...typeof meta.owner === "string" && meta.owner ? { owner: meta.owner } : {},
32295
+ ...titleLocked !== void 0 ? { titleLocked } : {},
32296
+ ...movedTo !== void 0 ? { movedTo } : {}
32071
32297
  };
32072
32298
  }
32073
32299
 
@@ -32477,9 +32703,37 @@ function validateRoomRequest(req) {
32477
32703
  "A browser cannot host a room: the messages and outcome a turn produces never leave the browser tab's worker, so the room would rotate blindly with every hand-over and close silently lost. Choose the daemon runtime instead."
32478
32704
  );
32479
32705
  }
32706
+ if (req.mode === "conversation") {
32707
+ if (!req.participants.some((p2) => p2.kind === "agent")) {
32708
+ problems.push(
32709
+ "A conversation room needs at least one agent: humans never hold the floor in this mode, so a room without one has nobody who can ever take a turn."
32710
+ );
32711
+ }
32712
+ if (req.presence === "wait") {
32713
+ problems.push(
32714
+ 'presence "wait" means waiting for a person to take their turn, and in a conversation room nobody does. Use "carry-on".'
32715
+ );
32716
+ }
32717
+ }
32480
32718
  if (!Number.isInteger(req.maxTurns) || req.maxTurns < 1) {
32481
32719
  problems.push("The turn cap must be a positive whole number.");
32482
32720
  }
32721
+ const ROLES = ["owner", "contributor", "viewer"];
32722
+ const owners = req.participants.filter((p2) => p2.kind === "human" && p2.role === "owner");
32723
+ if (owners.length > 1) {
32724
+ problems.push(
32725
+ `A room has one owner; this request names ${owners.length}: ${owners.map((p2) => p2.name).join(", ")}.`
32726
+ );
32727
+ }
32728
+ for (const p2 of req.participants) {
32729
+ const role = p2.role;
32730
+ if (role === void 0) continue;
32731
+ if (p2.kind === "agent") {
32732
+ problems.push(`"${p2.name}" is an agent and cannot hold a role: agents are the work, not participants who are granted access.`);
32733
+ } else if (!ROLES.includes(role)) {
32734
+ problems.push(`"${role}" is not a role. Known roles: ${ROLES.join(", ")}.`);
32735
+ }
32736
+ }
32483
32737
  return problems;
32484
32738
  }
32485
32739
  async function planOpen(req, reach, at2) {
@@ -32511,7 +32765,8 @@ async function planOpen(req, reach, at2) {
32511
32765
  brief: (req.brief ?? "").trim(),
32512
32766
  ...req.maxUsd !== void 0 ? { maxUsd: req.maxUsd } : {},
32513
32767
  runtime: req.runtime,
32514
- presence: req.presence
32768
+ presence: req.presence,
32769
+ ...req.mode !== void 0 ? { mode: req.mode } : {}
32515
32770
  }
32516
32771
  };
32517
32772
  }
@@ -42367,6 +42622,44 @@ function formatOffloadRef(ref) {
42367
42622
  ${ref.preview}${ref.bytes > ref.preview.length ? "\n\u2026(truncated)" : ""}` : head;
42368
42623
  }
42369
42624
 
42625
+ // ../harness/core/contextPressure.ts
42626
+ var CONTEXT_MARKER = "[context] ";
42627
+ var PRESSURE_BANDS = [0.6, 0.8, 0.9];
42628
+ var PRESSURE_HYSTERESIS = 0.05;
42629
+ var initialPressureGate = { crossed: 0 };
42630
+ function highestBandAtOrBelow(fill, slack = 0) {
42631
+ let best = 0;
42632
+ for (const b2 of PRESSURE_BANDS) if (fill >= b2 - slack) best = b2;
42633
+ return best;
42634
+ }
42635
+ function pressureDecision(fill, gate) {
42636
+ const current2 = highestBandAtOrBelow(fill);
42637
+ if (current2 > gate.crossed) return { band: current2, next: { crossed: current2 } };
42638
+ const held = highestBandAtOrBelow(fill, PRESSURE_HYSTERESIS);
42639
+ return { band: null, next: { crossed: Math.min(gate.crossed, held) } };
42640
+ }
42641
+ function pressureMessage(band, fill) {
42642
+ const pct = Math.round(fill * 100);
42643
+ switch (band) {
42644
+ case 0.6:
42645
+ return `${CONTEXT_MARKER}Context is ${pct}% full. Read files in ranges rather than whole, summarize tool output rather than quoting it back, and stop re-reading things already in this conversation.`;
42646
+ case 0.8:
42647
+ return `${CONTEXT_MARKER}Context is ${pct}% full. Finish the task in progress and report it; start no new exploration, and open nothing large. If the work does not fit, say what is left rather than beginning it.`;
42648
+ case 0.9:
42649
+ return `${CONTEXT_MARKER}Context is ${pct}% full. Stop gathering and answer now with what you already have, naming what you did not get to. Further reading will be lost.`;
42650
+ }
42651
+ }
42652
+ function contextWindowLine(contextWindow) {
42653
+ if (!contextWindow || contextWindow <= 0) return "";
42654
+ return `Your context window is ${contextWindow.toLocaleString("en-US")} tokens. You will be told when it is filling up.`;
42655
+ }
42656
+ function isContextNote(m2) {
42657
+ return m2.role === "assistant" && typeof m2.content === "string" && m2.content.startsWith(CONTEXT_MARKER);
42658
+ }
42659
+ function contextNote(band, fill) {
42660
+ return { role: "assistant", content: pressureMessage(band, fill) };
42661
+ }
42662
+
42370
42663
  // ../harness/core/compactHistory.ts
42371
42664
  var CHECKPOINT_MARKER = "[checkpoint] ";
42372
42665
  var ANCHOR_MARKER = "[anchor] ";
@@ -42416,11 +42709,11 @@ async function compactHistory(messages, opts) {
42416
42709
  const pending = tail3 && isUnansweredToolCall(tail3) ? tail3 : null;
42417
42710
  const body2 = pending ? messages.slice(0, -1) : messages;
42418
42711
  const system = body2.filter((m2) => m2.role === "system");
42419
- const collapsed = body2.filter((m2) => m2.role !== "system" && m2.role !== "user");
42712
+ const collapsed = body2.filter((m2) => m2.role !== "system" && m2.role !== "user" && !isContextNote(m2));
42420
42713
  const kept = [];
42421
42714
  let answered = null;
42422
42715
  for (const m2 of body2) {
42423
- if (isOwnMarker(m2)) continue;
42716
+ if (isOwnMarker(m2) || isContextNote(m2)) continue;
42424
42717
  if (m2.role === "assistant" && typeof m2.content === "string") answered = m2;
42425
42718
  if (m2.role !== "user") continue;
42426
42719
  if (opts.anchor) {
@@ -42574,9 +42867,11 @@ async function runAgent(args) {
42574
42867
  const ctxWindow = compression?.contextWindow;
42575
42868
  const compressionStrategy = compression?.strategy ?? "eager";
42576
42869
  let fill = 0;
42870
+ let pressureGate = initialPressureGate;
42577
42871
  let compactGate = { armed: true, floor: null };
42578
42872
  const deferredOffloads = [];
42579
- const maxTurns = policy.maxTurns ?? args.maxTurns ?? 8;
42873
+ const maxTurns = policy.maxTurns !== void 0 ? policy.maxTurns : args.maxTurns !== void 0 ? args.maxTurns : 40;
42874
+ const unlimitedTurns = maxTurns === 0;
42580
42875
  const signal = args.signal ?? new AbortController().signal;
42581
42876
  const emit = (e2) => args.onEvent?.(e2);
42582
42877
  const gstate = initialState();
@@ -42608,9 +42903,8 @@ async function runAgent(args) {
42608
42903
  const capped = (list) => list.length > MAX_PROVIDER_TOOLS ? list.slice(0, MAX_PROVIDER_TOOLS) : list;
42609
42904
  const activeTools = () => capped(loader ? [...eager, ...loader.loaded()] : eager);
42610
42905
  let sent = activeTools();
42611
- const indexedSystem = loader ? system ? `${system}
42612
-
42613
- ${serverIndex(deferred)}` : serverIndex(deferred) : system;
42906
+ const systemParts = [system, loader ? serverIndex(deferred) : "", contextWindowLine(ctxWindow)].filter(Boolean);
42907
+ const indexedSystem = systemParts.length ? systemParts.join("\n\n") : system;
42614
42908
  const filledSystem = indexedSystem ? fillToolsMarker(indexedSystem, sent) : indexedSystem;
42615
42909
  if (filledSystem && !messages.some((m2) => m2.role === "system")) {
42616
42910
  messages.unshift({ role: "system", content: filledSystem });
@@ -42630,7 +42924,7 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
42630
42924
  let stopReason = "stop";
42631
42925
  let lastToolSig = null;
42632
42926
  let lastStreamErr = "";
42633
- for (let turn = 1; turn <= maxTurns; turn++) {
42927
+ for (let turn = 1; unlimitedTurns || turn <= maxTurns; turn++) {
42634
42928
  if (signal.aborted) {
42635
42929
  stopReason = "aborted";
42636
42930
  break;
@@ -42668,13 +42962,28 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
42668
42962
  alog("compaction failed", e2 instanceof Error ? e2.message : String(e2));
42669
42963
  }
42670
42964
  }
42671
- gstate.turn = turn - 1;
42672
- const turnGate = evaluateTurnStart(policy, gstate);
42673
- if (!turnGate.ok) {
42674
- emit({ type: "policy-violation", kind: turnGate.violation.kind, message: turnGate.violation.message, action: turnGate.action, tool: turnGate.violation.tool });
42675
- if (turnGate.action === "stop") {
42676
- stopReason = "policy";
42677
- break;
42965
+ const pressureFill = fill || estimateFill(messages, ctxWindow);
42966
+ const pressure = pressureDecision(pressureFill, pressureGate);
42967
+ pressureGate = pressure.next;
42968
+ if (pressure.band !== null && ctxWindow) {
42969
+ messages.push(contextNote(pressure.band, pressureFill));
42970
+ emit({
42971
+ type: "context-pressure",
42972
+ band: pressure.band,
42973
+ fill: pressureFill,
42974
+ contextWindow: ctxWindow,
42975
+ promptTokens: Math.round(pressureFill * ctxWindow)
42976
+ });
42977
+ }
42978
+ if (!unlimitedTurns) {
42979
+ gstate.turn = turn - 1;
42980
+ const turnGate = evaluateTurnStart(policy, gstate);
42981
+ if (!turnGate.ok) {
42982
+ emit({ type: "policy-violation", kind: turnGate.violation.kind, message: turnGate.violation.message, action: turnGate.action, tool: turnGate.violation.tool });
42983
+ if (turnGate.action === "stop") {
42984
+ stopReason = "policy";
42985
+ break;
42986
+ }
42678
42987
  }
42679
42988
  }
42680
42989
  if (loader) {
@@ -42892,7 +43201,7 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
42892
43201
  stopReason = "policy";
42893
43202
  break;
42894
43203
  }
42895
- if (turn === maxTurns) stopReason = "max_turns";
43204
+ if (!unlimitedTurns && turn === maxTurns) stopReason = "max_turns";
42896
43205
  }
42897
43206
  reconcileDanglingToolCalls(messages);
42898
43207
  for (const v2 of evaluateCompletion(policy, gstate)) {
@@ -42976,9 +43285,10 @@ function now() {
42976
43285
  return lastTs;
42977
43286
  }
42978
43287
  var JsonlSessionStore = class _JsonlSessionStore {
42979
- constructor(getFiles, root = sessionsRoot) {
43288
+ constructor(getFiles, root = sessionsRoot, owner = () => void 0) {
42980
43289
  this.getFiles = getFiles;
42981
43290
  this.root = root;
43291
+ this.owner = owner;
42982
43292
  }
42983
43293
  cursors = /* @__PURE__ */ new Map();
42984
43294
  async files() {
@@ -43013,11 +43323,13 @@ var JsonlSessionStore = class _JsonlSessionStore {
43013
43323
  async create(title, surface) {
43014
43324
  const createdAt = now();
43015
43325
  const id = uid("ses");
43016
- const meta = { k: "meta", id, title, createdAt, ...surface ? { surface } : {} };
43326
+ const owner = this.owner();
43327
+ const own = owner ? { owner } : {};
43328
+ const meta = { k: "meta", id, title, createdAt, ...surface ? { surface } : {}, ...own };
43017
43329
  const text = encodeLine(meta);
43018
43330
  await (await this.files()).write(this.path(id), text);
43019
43331
  this.cursors.set(id, { text, turnJson: [], title });
43020
- return { id, title, createdAt, updatedAt: createdAt, messages: [], ...surface ? { surface } : {} };
43332
+ return { id, title, createdAt, updatedAt: createdAt, messages: [], ...surface ? { surface } : {}, ...own };
43021
43333
  }
43022
43334
  async get(id) {
43023
43335
  const text = await (await this.files()).read(this.path(id)).catch(() => null);
@@ -43160,7 +43472,8 @@ var JsonlSessionStore = class _JsonlSessionStore {
43160
43472
  id: session.id,
43161
43473
  title: session.title,
43162
43474
  createdAt: session.createdAt,
43163
- ...session.surface ? { surface: session.surface } : {}
43475
+ ...session.surface ? { surface: session.surface } : {},
43476
+ ...session.owner ? { owner: session.owner } : {}
43164
43477
  });
43165
43478
  return { text: meta, turnJson: [], title: session.title, titleLocked: void 0 };
43166
43479
  }
@@ -43347,30 +43660,43 @@ function pricedForUsd(rates) {
43347
43660
  if (!rates) return false;
43348
43661
  return rates.inPrice > 0 || rates.outPrice > 0;
43349
43662
  }
43350
- async function hydratePrices(endpoint, getToken, fetchImpl = fetch) {
43351
- const table = /* @__PURE__ */ new Map();
43663
+ async function fetchAvailableModels(endpoint, getToken, fetchImpl) {
43352
43664
  let token = null;
43353
43665
  try {
43354
43666
  token = await getToken();
43355
43667
  } catch {
43356
43668
  token = null;
43357
43669
  }
43358
- if (!token) return table;
43670
+ if (!token) return [];
43359
43671
  try {
43360
43672
  const res = await fetchImpl(`${endpoint.replace(/\/$/, "")}/llm-providers/available`, {
43361
43673
  headers: { Authorization: `Bearer ${token}` }
43362
43674
  });
43363
- if (!res.ok) return table;
43675
+ if (!res.ok) return [];
43364
43676
  const json = await res.json();
43365
- for (const p2 of json.providers ?? []) {
43366
- for (const m2 of p2.models ?? []) {
43367
- if (m2.inputPrice == null || m2.outputPrice == null) continue;
43368
- table.set(m2.model, { inPrice: m2.inputPrice, outPrice: m2.outputPrice });
43369
- }
43370
- }
43677
+ const out = [];
43678
+ for (const p2 of json.providers ?? []) out.push(...p2.models ?? []);
43679
+ return out;
43371
43680
  } catch {
43681
+ return [];
43682
+ }
43683
+ }
43684
+ async function hydrateCatalog(endpoint, getToken, fetchImpl = fetch) {
43685
+ const prices = /* @__PURE__ */ new Map();
43686
+ const windows = /* @__PURE__ */ new Map();
43687
+ const models = await fetchAvailableModels(endpoint, getToken, fetchImpl);
43688
+ for (const m2 of models) {
43689
+ if (m2.inputPrice != null && m2.outputPrice != null) {
43690
+ prices.set(m2.model, { inPrice: m2.inputPrice, outPrice: m2.outputPrice });
43691
+ }
43692
+ if (typeof m2.contextWindow === "number" && m2.contextWindow > 0) {
43693
+ windows.set(m2.model, m2.contextWindow);
43694
+ }
43372
43695
  }
43373
- return table;
43696
+ return { prices, windows };
43697
+ }
43698
+ async function hydratePrices(endpoint, getToken, fetchImpl = fetch) {
43699
+ return (await hydrateCatalog(endpoint, getToken, fetchImpl)).prices;
43374
43700
  }
43375
43701
 
43376
43702
  // src/runner/permissionMessage.ts
@@ -43665,7 +43991,7 @@ function makeUnattendedPrompt(deps) {
43665
43991
  for (; ; ) {
43666
43992
  if (remaining <= 0) return { by: "timeout" };
43667
43993
  const tickMs = Math.min(remaining, PARK_POLL_MS);
43668
- if (deps.registry.isAttended()) {
43994
+ if (deps.registry.isAttended(deps.owner)) {
43669
43995
  const ctrl2 = new AbortController();
43670
43996
  const answered = deps.registry.broadcastAsk(ask, ctrl2.signal);
43671
43997
  const ticked = sleep2(tickMs, ctrl2.signal).then(() => "tick");
@@ -43703,7 +44029,11 @@ function makeUnattendedPrompt(deps) {
43703
44029
  agent: deps.agent,
43704
44030
  tool: req.tool,
43705
44031
  input: req.input,
43706
- deadline: now2() + bound
44032
+ deadline: now2() + bound,
44033
+ // Spread-when-present rather than `owner: deps.owner`, so an ownerless
44034
+ // ask has no `owner` KEY at all -- the registry's `!ask.owner` test
44035
+ // stays unambiguous and a serialised ask does not carry a null.
44036
+ ...deps.owner ? { owner: deps.owner } : {}
43707
44037
  };
43708
44038
  if (deps.unattended === "park") {
43709
44039
  deps.onParkChange?.(true);
@@ -43723,7 +44053,7 @@ function makeUnattendedPrompt(deps) {
43723
44053
  }
43724
44054
  }
43725
44055
  const fallthrough = deps.unattended === "allow" ? "allow" : "deny";
43726
- if (deps.registry.isAttended()) {
44056
+ if (deps.registry.isAttended(deps.owner)) {
43727
44057
  deps.onParkChange?.(true);
43728
44058
  deps.onAskWait?.(ask);
43729
44059
  try {
@@ -44125,6 +44455,21 @@ var NodeRunnerService = class {
44125
44455
  registry: registry2,
44126
44456
  runId: sessionId,
44127
44457
  agent,
44458
+ /* WHOSE RUN THIS IS, for `--mode team`'s ask routing.
44459
+ *
44460
+ * Only the `{ clientId }` variant names a connection. `{ owner }` is
44461
+ * a dirName the daemon's own trigger dispatcher resolved from
44462
+ * filesystem enumeration -- there is no socket behind it, so an ask
44463
+ * it raises has nobody to route to and correctly falls back to
44464
+ * fan-out. Same for a `{ clientId }` of `undefined` (an older peer).
44465
+ *
44466
+ * Deliberately the RAW clientId rather than `owner` (the resolved
44467
+ * dirName computed above): this value is only ever compared against
44468
+ * the identity a connection ATTACHED with, which is the same
44469
+ * authenticated clientId the harness filed it under. It is never
44470
+ * used as a path, so the manifest resolution that `owner` needs
44471
+ * would not make it safer -- it would just make it fail to match. */
44472
+ ..."owner" in identity || identity.clientId === void 0 ? {} : { owner: identity.clientId },
44128
44473
  unattended: def.policy.unattended ?? "deny",
44129
44474
  ...def.policy.parkTimeoutMs != null ? { parkTimeoutMs: def.policy.parkTimeoutMs } : {},
44130
44475
  remainingMs,
@@ -44906,6 +45251,20 @@ function evaluateRunComplete(completion, agents, maxChainDepth, log) {
44906
45251
  }
44907
45252
 
44908
45253
  // src/runner/roomService.ts
45254
+ function hasRecordedOwner(state) {
45255
+ return state.participants.some((p2) => p2.kind === "human" && p2.role === "owner");
45256
+ }
45257
+ function mayAct(state, participantId, verb) {
45258
+ const member = state.participants.find((p2) => p2.id === participantId);
45259
+ if (!member) return `"${participantId}" is not a member of this room.`;
45260
+ const role = roleOf(member);
45261
+ if (role === null) return `"${participantId}" is an agent, not a participant who acts on the room.`;
45262
+ if (verb === "close") {
45263
+ if (!hasRecordedOwner(state)) return null;
45264
+ return role === "owner" ? null : `Only the owner can ${verb} this room; you are a ${role}.`;
45265
+ }
45266
+ return role === "viewer" ? `You are a viewer in this room and cannot ${verb}.` : null;
45267
+ }
44909
45268
  function isDepsFactory(d2) {
44910
45269
  return typeof d2 === "function";
44911
45270
  }
@@ -45027,6 +45386,11 @@ function makeRoomService(opts) {
45027
45386
  }
45028
45387
  async function open(req) {
45029
45388
  return enqueue(req.roomId, async () => {
45389
+ if (req.fromSession && req.fromSession.messages.length !== req.fromSession.turns) {
45390
+ const message = `Room "${req.roomId}" was not opened: the promoted thread arrived with ${req.fromSession.messages.length} message(s) but ${req.fromSession.turns} were expected. Refusing to open a room missing part of the conversation.`;
45391
+ log(`room "${req.roomId}" could not be opened: ${message}`);
45392
+ return { ok: false, problems: [message] };
45393
+ }
45030
45394
  let deps;
45031
45395
  try {
45032
45396
  deps = await resolveDeps(req.participants, req.maxUsd ?? null);
@@ -45037,6 +45401,15 @@ function makeRoomService(opts) {
45037
45401
  }
45038
45402
  const opened = await openRoom(req, opts.reachability, deps);
45039
45403
  if (!opened.ok) return opened;
45404
+ if (req.fromSession) {
45405
+ try {
45406
+ await deps.store.appendTurns(req.roomId, 0, req.fromSession.messages, deps.now());
45407
+ } catch (err) {
45408
+ const message = err instanceof Error ? err.message : String(err);
45409
+ log(`room "${req.roomId}" opened but its promoted transcript failed to append: ${message}`);
45410
+ return { ok: false, problems: [`Room "${req.roomId}" opened, but the promoted conversation could not be saved into it: ${message}`] };
45411
+ }
45412
+ }
45040
45413
  depsByRoom.set(req.roomId, deps);
45041
45414
  const room = summaryOf(opened.state, null, null, 0);
45042
45415
  record(room);
@@ -45092,10 +45465,22 @@ function makeRoomService(opts) {
45092
45465
  }
45093
45466
  return fresh;
45094
45467
  }
45468
+ async function requireRole(deps, roomId, participantId, verb) {
45469
+ const state = await deps.store.read(roomId);
45470
+ if (!state || state.closed) return;
45471
+ const refusal = mayAct(state, participantId, verb);
45472
+ if (refusal) throw new Error(refusal);
45473
+ }
45095
45474
  async function interject(roomId, participantId, text) {
45096
45475
  return enqueue(roomId, async () => {
45097
45476
  const deps = await baseDeps();
45477
+ await requireRole(deps, roomId, participantId, "interject");
45098
45478
  await interjectHuman(roomId, participantId, text, deps);
45479
+ const state = await deps.store.read(roomId);
45480
+ if (state && !state.closed && state.mode === "conversation") {
45481
+ const driveDeps = await depsFor(roomId);
45482
+ if (driveDeps) return driveRoom(roomId, driveDeps);
45483
+ }
45099
45484
  const summary = await summaryNow(roomId);
45100
45485
  record(summary);
45101
45486
  return summary;
@@ -45104,6 +45489,7 @@ function makeRoomService(opts) {
45104
45489
  async function closeRoom(roomId, participantId, reason) {
45105
45490
  return enqueue(roomId, async () => {
45106
45491
  const deps = await baseDeps();
45492
+ await requireRole(deps, roomId, participantId, "close");
45107
45493
  await closeRoomByHuman(roomId, participantId, reason, deps);
45108
45494
  const summary = await summaryNow(roomId);
45109
45495
  record(summary);
@@ -45117,6 +45503,7 @@ function makeRoomService(opts) {
45117
45503
  async function editFinding(roomId, participantId, edit) {
45118
45504
  return enqueue(roomId, async () => {
45119
45505
  const deps = await baseDeps();
45506
+ await requireRole(deps, roomId, participantId, "finding-edit");
45120
45507
  const doc = await editFindingByHuman(roomId, participantId, edit, deps);
45121
45508
  record(await summaryNow(roomId));
45122
45509
  return doc;
@@ -45126,6 +45513,7 @@ function makeRoomService(opts) {
45126
45513
  return enqueue(roomId, async () => {
45127
45514
  const deps = await depsFor(roomId);
45128
45515
  if (!deps) throw new Error(`No room "${roomId}".`);
45516
+ await requireRole(deps, roomId, participantId, "answer");
45129
45517
  await answerHumanTurn(roomId, participantId, text, deps, addressed);
45130
45518
  return driveRoom(roomId, deps);
45131
45519
  });
@@ -45692,27 +46080,49 @@ function startDaemonCore(opts) {
45692
46080
  function makeAttendanceRegistry(opts) {
45693
46081
  const log = opts?.log ?? (() => {
45694
46082
  });
46083
+ const teamMode = opts?.teamMode === true;
46084
+ const EMPTY = /* @__PURE__ */ new Set();
45695
46085
  const clients = /* @__PURE__ */ new Set();
46086
+ const identities = /* @__PURE__ */ new Map();
45696
46087
  let lastRegistrations = null;
45697
46088
  const outstanding = /* @__PURE__ */ new Map();
45698
46089
  let idCounter = 0;
45699
- const notifyResolved = (id, decision, by) => {
46090
+ const recipients = (ask) => {
46091
+ if (!teamMode || !ask.owner) return clients;
46092
+ const owners = [];
46093
+ const ownerAccounts = /* @__PURE__ */ new Set();
46094
+ for (const [client, id] of identities) {
46095
+ if (id.clientId !== ask.owner) continue;
46096
+ owners.push(client);
46097
+ for (const a2 of id.accounts) ownerAccounts.add(a2);
46098
+ }
46099
+ if (owners.length === 0) return EMPTY;
46100
+ const out = new Set(owners);
46101
+ if (ownerAccounts.size > 0) {
46102
+ for (const [client, id] of identities) {
46103
+ if (id.accounts.some((a2) => ownerAccounts.has(a2))) out.add(client);
46104
+ }
46105
+ }
46106
+ return out;
46107
+ };
46108
+ const notifyResolved = (id, decision, by, byClientId) => {
45700
46109
  for (const c2 of clients) {
45701
- c2.resolved(id, decision, by);
46110
+ c2.resolved(id, decision, by, byClientId);
45702
46111
  }
45703
46112
  };
45704
- const resolveEntry = (id, decision, by) => {
46113
+ const resolveEntry = (id, decision, by, byClientId) => {
45705
46114
  const entry = outstanding.get(id);
45706
46115
  if (!entry) return;
45707
46116
  outstanding.delete(id);
45708
46117
  entry.resolve(decision);
45709
- notifyResolved(id, decision, by);
46118
+ notifyResolved(id, decision, by, byClientId);
45710
46119
  };
45711
46120
  return {
45712
- attach(client) {
46121
+ attach(client, identity) {
45713
46122
  clients.add(client);
46123
+ if (identity) identities.set(client, identity);
45714
46124
  for (const { ask } of outstanding.values()) {
45715
- client.send(ask);
46125
+ if (recipients(ask).has(client)) client.send(ask);
45716
46126
  }
45717
46127
  if (lastRegistrations) {
45718
46128
  try {
@@ -45725,6 +46135,7 @@ function makeAttendanceRegistry(opts) {
45725
46135
  if (detached) return;
45726
46136
  detached = true;
45727
46137
  clients.delete(client);
46138
+ identities.delete(client);
45728
46139
  if (clients.size === 0 && outstanding.size > 0) {
45729
46140
  log(`attendance: last client detached with ${outstanding.size} ask(s) outstanding; resolving null`);
45730
46141
  for (const [id, entry] of Array.from(outstanding.entries())) {
@@ -45734,14 +46145,22 @@ function makeAttendanceRegistry(opts) {
45734
46145
  }
45735
46146
  };
45736
46147
  },
45737
- isAttended() {
45738
- return clients.size > 0;
46148
+ attachedIdentities() {
46149
+ return Array.from(identities.values());
46150
+ },
46151
+ isAttended(owner) {
46152
+ if (!teamMode || !owner) return clients.size > 0;
46153
+ for (const id of identities.values()) {
46154
+ if (id.clientId === owner) return true;
46155
+ }
46156
+ return false;
45739
46157
  },
45740
46158
  outstanding() {
45741
46159
  return Array.from(outstanding.values()).map((e2) => e2.ask);
45742
46160
  },
45743
46161
  broadcastAsk(ask, signal) {
45744
- if (clients.size === 0) {
46162
+ const to = recipients(ask);
46163
+ if (to.size === 0) {
45745
46164
  return Promise.resolve(null);
45746
46165
  }
45747
46166
  return new Promise((resolve14) => {
@@ -45759,7 +46178,7 @@ function makeAttendanceRegistry(opts) {
45759
46178
  resolve14(decision);
45760
46179
  }
45761
46180
  });
45762
- for (const c2 of clients) {
46181
+ for (const c2 of to) {
45763
46182
  c2.send(ask);
45764
46183
  }
45765
46184
  if (signal) {
@@ -45789,8 +46208,9 @@ function makeAttendanceRegistry(opts) {
45789
46208
  }
45790
46209
  }
45791
46210
  },
45792
- answer(id, decision) {
45793
- resolveEntry(id, decision, "client");
46211
+ answer(id, decision, by) {
46212
+ const named = by ? identities.get(by)?.clientId : void 0;
46213
+ resolveEntry(id, decision, "client", named);
45794
46214
  },
45795
46215
  settle(id, decision, by) {
45796
46216
  resolveEntry(id, decision, by);
@@ -47188,6 +47608,7 @@ var FLAG_TO_KEY = {
47188
47608
  "--clone-under": "cloneUnder",
47189
47609
  "--allow-origin": "allowOrigins",
47190
47610
  "--allow-lan": "allowLan",
47611
+ "--mode": "teamMode",
47191
47612
  "--allow-shell": "allowShell",
47192
47613
  "--keep-transcripts": "keepTranscripts",
47193
47614
  "--isolation": "isolation",
@@ -47206,7 +47627,7 @@ var FLAG_TO_KEY = {
47206
47627
  "--default-model": "defaultModel",
47207
47628
  "--name": "name"
47208
47629
  };
47209
- var DEFAULT_GUEST_IMAGE2 = "prompd/corenel-sandbox:0.8.8";
47630
+ var DEFAULT_GUEST_IMAGE2 = "prompd/corenel-sandbox:0.10.0";
47210
47631
  function parseArgsWithProvenance(argv) {
47211
47632
  const explicit = /* @__PURE__ */ new Set();
47212
47633
  for (const a2 of argv) {
@@ -47223,6 +47644,7 @@ function parseArgs(argv) {
47223
47644
  allowOrigins: [],
47224
47645
  allowShell: false,
47225
47646
  allowLan: false,
47647
+ teamMode: false,
47226
47648
  keepTranscripts: false,
47227
47649
  isolation: "none",
47228
47650
  msbBin: process.env.MSB_BIN ?? "msb",
@@ -47277,7 +47699,11 @@ function parseArgs(argv) {
47277
47699
  throw new Error(`--snapshot-budget: ${e2 instanceof Error ? e2.message : String(e2)}`);
47278
47700
  }
47279
47701
  } else if (a2 === "--allow-lan") out.allowLan = true;
47280
- else if (a2 === "--tls-cert") out.tlsCert = resolve10(argv[++i2]);
47702
+ else if (a2 === "--mode") {
47703
+ const mode = argv[++i2];
47704
+ if (mode !== "team") throw new Error(`unknown --mode "${mode}". Known modes: team`);
47705
+ out.teamMode = true;
47706
+ } else if (a2 === "--tls-cert") out.tlsCert = resolve10(argv[++i2]);
47281
47707
  else if (a2 === "--tls-key") out.tlsKey = resolve10(argv[++i2]);
47282
47708
  else if (a2 === "--self-signed") out.selfSigned = true;
47283
47709
  else if (a2 === "--tunnel") out.tunnel = argv[++i2];
@@ -47399,6 +47825,17 @@ function layerConfig(args, explicit, file) {
47399
47825
  from[key] = "file";
47400
47826
  if (key === "endpoint") out.endpointExplicit = true;
47401
47827
  }
47828
+ if (explicit.has("teamMode")) {
47829
+ from.teamMode = "flag";
47830
+ } else if (file.mode === void 0) {
47831
+ from.teamMode = "default";
47832
+ } else if (file.mode === "team") {
47833
+ out.teamMode = true;
47834
+ from.teamMode = "file";
47835
+ } else {
47836
+ warnings.push(`daemon.json: "mode" must be "team", got ${JSON.stringify(file.mode)} -- ignoring it, using the default`);
47837
+ from.teamMode = "default";
47838
+ }
47402
47839
  if (out.allowLan) {
47403
47840
  if (from.host === "default") out.host = "0.0.0.0";
47404
47841
  if (from.selfSigned === "default" && !out.tlsCert && !out.tlsKey) out.selfSigned = true;
@@ -47580,6 +48017,11 @@ var KEY_TYPES = {
47580
48017
  allowShell: "boolean",
47581
48018
  keepTranscripts: "boolean",
47582
48019
  isolation: "string",
48020
+ // A string, not a boolean, so the file spells it the way the flag does
48021
+ // (`--mode team`) and a second mode needs no second key. layer.ts refuses
48022
+ // any value but 'team', with a warning -- typed loosely here, validated
48023
+ // strictly there, the same split `isolation` has.
48024
+ mode: "string",
47583
48025
  msbBin: "string",
47584
48026
  guestImage: "string",
47585
48027
  maxChainDepth: "number",
@@ -51071,7 +51513,7 @@ async function main() {
51071
51513
  // schedule looks broken for a quarter of an hour.
51072
51514
  onWrite: () => core?.wake()
51073
51515
  });
51074
- const attendance = makeAttendanceRegistry({ log: slog });
51516
+ const attendance = makeAttendanceRegistry({ log: slog, teamMode: args.teamMode });
51075
51517
  const runs = makeRunHub({ log: slog });
51076
51518
  let core;
51077
51519
  let crewSessions;
@@ -51387,6 +51829,10 @@ async function main() {
51387
51829
  // (roomCtx above) -- absent otherwise, so `welcome.features` omits
51388
51830
  // 'rooms' exactly like every other optional daemon capability here.
51389
51831
  rooms: core?.rooms,
51832
+ // Same operator flag `attendance.ts` already reads for ask routing --
51833
+ // `room-list` applies the identical private-by-default narrowing to
51834
+ // rooms once this is on (see `onRoomList`'s R7 comment).
51835
+ teamMode: args.teamMode,
51390
51836
  // The pause control plane. Absent without --daemon, so 'agent-pause' is not
51391
51837
  // advertised and a client correctly reports that this daemon holds none.
51392
51838
  ...pauses ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corenel/sidecar",
3
- "version": "0.8.8",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "license": "Elastic-2.0",
6
6
  "description": "Prompd sidecar — a local capability server. Run it next to Prompd (prompd.app) so the browser app can reach your machine: local LLMs (Ollama/LM Studio/vLLM) without CORS, your filesystem, and (opt-in) shell.",
@@ -36,11 +36,11 @@
36
36
  "esbuild": "^0.21.5",
37
37
  "tsx": "^4.19.2",
38
38
  "typescript": "^5.9.3",
39
- "@corenel/config": "0.1.0",
40
- "@corenel/mcp": "0.1.0",
41
- "@corenel/protocol": "0.7.3",
42
- "@corenel/tools-node": "0.4.0",
43
- "@corenel/harness": "0.8.0",
39
+ "@corenel/config": "0.3.0",
40
+ "@corenel/mcp": "0.2.0",
41
+ "@corenel/protocol": "0.9.0",
42
+ "@corenel/harness": "0.10.0",
43
+ "@corenel/tools-node": "0.5.0",
44
44
  "@corenel/crew": "0.0.0"
45
45
  },
46
46
  "repository": {