replicas-engine 0.1.567 → 0.1.569

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/src/index.js +128 -42
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -2114,7 +2114,7 @@ LENGTH=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE")
2114
2114
 
2115
2115
  # 1. Reserve an upload URL + file ID.
2116
2116
  RESERVE=$(curl -s -G "https://slack.com/api/files.getUploadURLExternal" \\
2117
- -H "Authorization: Bearer $SLACK_BOT_TOKEN" \\
2117
+ -H "Authorization: Bearer $REPLICAS_SLACK_BOT_TOKEN" \\
2118
2118
  --data-urlencode "filename=$FILENAME" \\
2119
2119
  --data-urlencode "length=$LENGTH")
2120
2120
  UPLOAD_URL=$(echo "$RESERVE" | jq -r '.upload_url')
@@ -2129,7 +2129,7 @@ curl -s -X POST "$UPLOAD_URL" \\
2129
2129
 
2130
2130
  # 3. Complete the upload, sharing it into the channel/thread with the dashboard link.
2131
2131
  curl -s -X POST "https://slack.com/api/files.completeUploadExternal" \\
2132
- -H "Authorization: Bearer $SLACK_BOT_TOKEN" \\
2132
+ -H "Authorization: Bearer $REPLICAS_SLACK_BOT_TOKEN" \\
2133
2133
  -H "Content-Type: application/json; charset=utf-8" \\
2134
2134
  -d "{
2135
2135
  \\"files\\": [{\\"id\\": \\"$FILE_ID\\", \\"title\\": \\"Screenshot\\"}],
@@ -2640,10 +2640,10 @@ This guide covers how to interact with Slack from within your Replicas workspace
2640
2640
 
2641
2641
  ## Prerequisites
2642
2642
 
2643
- Check if the \`SLACK_BOT_TOKEN\` environment variable is set:
2643
+ Check if the \`REPLICAS_SLACK_BOT_TOKEN\` environment variable is set:
2644
2644
 
2645
2645
  \`\`\`bash
2646
- echo "\${SLACK_BOT_TOKEN:+set}"
2646
+ echo "\${REPLICAS_SLACK_BOT_TOKEN:+set}"
2647
2647
  \`\`\`
2648
2648
 
2649
2649
  - If **set**: Your workspace has Slack access. You can use the Slack Web API as described below.
@@ -2651,7 +2651,7 @@ echo "\${SLACK_BOT_TOKEN:+set}"
2651
2651
 
2652
2652
  ## Using the Slack API
2653
2653
 
2654
- All requests use the \`$SLACK_BOT_TOKEN\` for authentication via the Slack Web API.
2654
+ All requests use the \`$REPLICAS_SLACK_BOT_TOKEN\` for authentication via the Slack Web API.
2655
2655
 
2656
2656
  ### Fetching a Thread from a Slack Link
2657
2657
 
@@ -2662,14 +2662,14 @@ If you encounter a Slack message link (e.g. \`https://team.slack.com/archives/C0
2662
2662
 
2663
2663
  \`\`\`bash
2664
2664
  curl -s "https://slack.com/api/conversations.replies?channel=CHANNEL_ID&ts=THREAD_TS" \\
2665
- -H "Authorization: Bearer $SLACK_BOT_TOKEN"
2665
+ -H "Authorization: Bearer $REPLICAS_SLACK_BOT_TOKEN"
2666
2666
  \`\`\`
2667
2667
 
2668
2668
  ### Sending a Message
2669
2669
 
2670
2670
  \`\`\`bash
2671
2671
  curl -s -X POST "https://slack.com/api/chat.postMessage" \\
2672
- -H "Authorization: Bearer $SLACK_BOT_TOKEN" \\
2672
+ -H "Authorization: Bearer $REPLICAS_SLACK_BOT_TOKEN" \\
2673
2673
  -H "Content-Type: application/json" \\
2674
2674
  -d '{
2675
2675
  "channel": "CHANNEL_ID",
@@ -2708,7 +2708,7 @@ The channel and thread flags default to \`SLACK_CHANNEL_ID\` and \`SLACK_THREAD_
2708
2708
 
2709
2709
  \`\`\`bash
2710
2710
  curl -s "https://slack.com/api/search.messages?query=YOUR_SEARCH_QUERY" \\
2711
- -H "Authorization: Bearer $SLACK_BOT_TOKEN"
2711
+ -H "Authorization: Bearer $REPLICAS_SLACK_BOT_TOKEN"
2712
2712
  \`\`\`
2713
2713
 
2714
2714
  ### Uploading Files
@@ -2722,7 +2722,7 @@ LENGTH=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE")
2722
2722
 
2723
2723
  # 1. Reserve an upload URL + file ID.
2724
2724
  RESERVE=$(curl -s -G "https://slack.com/api/files.getUploadURLExternal" \\
2725
- -H "Authorization: Bearer $SLACK_BOT_TOKEN" \\
2725
+ -H "Authorization: Bearer $REPLICAS_SLACK_BOT_TOKEN" \\
2726
2726
  --data-urlencode "filename=$FILENAME" \\
2727
2727
  --data-urlencode "length=$LENGTH")
2728
2728
  UPLOAD_URL=$(echo "$RESERVE" | jq -r '.upload_url')
@@ -2737,7 +2737,7 @@ curl -s -X POST "$UPLOAD_URL" \\
2737
2737
 
2738
2738
  # 3. Complete the upload and share into a channel (and optionally a thread).
2739
2739
  curl -s -X POST "https://slack.com/api/files.completeUploadExternal" \\
2740
- -H "Authorization: Bearer $SLACK_BOT_TOKEN" \\
2740
+ -H "Authorization: Bearer $REPLICAS_SLACK_BOT_TOKEN" \\
2741
2741
  -H "Content-Type: application/json; charset=utf-8" \\
2742
2742
  -d "{
2743
2743
  \\"files\\": [{\\"id\\": \\"$FILE_ID\\", \\"title\\": \\"File title\\"}],
@@ -4260,8 +4260,9 @@ function parseOpencodeEvents(events) {
4260
4260
  if (event.type === "event_msg" && event.payload.type === "user_message") {
4261
4261
  const message = event.payload.message;
4262
4262
  if (typeof message === "string" && message.trim()) {
4263
+ const messageId = event.payload[USER_MESSAGE_ID_PAYLOAD_KEY];
4263
4264
  messages.push({
4264
- id: `opencode-user-${event.timestamp}-${messages.length}`,
4265
+ id: typeof messageId === "string" ? messageId : `opencode-user-${event.timestamp}`,
4265
4266
  type: "user",
4266
4267
  content: message,
4267
4268
  timestamp: event.timestamp
@@ -4273,7 +4274,7 @@ function parseOpencodeEvents(events) {
4273
4274
  const error = assistantError(event.payload.info);
4274
4275
  if (error) {
4275
4276
  messages.push({
4276
- id: `opencode-error-${event.timestamp}-${messages.length}`,
4277
+ id: `opencode-${event.type}-${event.timestamp}`,
4277
4278
  type: "error",
4278
4279
  message: error,
4279
4280
  timestamp: event.timestamp
@@ -4284,7 +4285,7 @@ function parseOpencodeEvents(events) {
4284
4285
  if (event.type === "opencode-error") {
4285
4286
  const message = typeof event.payload.message === "string" ? event.payload.message : "Opencode run failed";
4286
4287
  messages.push({
4287
- id: `opencode-error-${event.timestamp}-${messages.length}`,
4288
+ id: `opencode-${event.type}-${event.timestamp}`,
4288
4289
  type: "error",
4289
4290
  message,
4290
4291
  timestamp: event.timestamp
@@ -4295,7 +4296,7 @@ function parseOpencodeEvents(events) {
4295
4296
  const tool2 = typeof event.payload.tool === "string" ? event.payload.tool : "tool";
4296
4297
  const input = event.payload.input ?? event.payload.arguments;
4297
4298
  const callId = typeof event.payload.callID === "string" ? event.payload.callID : null;
4298
- const id2 = callId ? `opencode-${callId}` : `opencode-${event.timestamp}-${messages.length}`;
4299
+ const id2 = callId ? `opencode-${callId}` : `opencode-${event.type}-${event.timestamp}`;
4299
4300
  upsertDisplayMessage(messages, createCallDisplayMessage({
4300
4301
  id: id2,
4301
4302
  server: "opencode",
@@ -4308,7 +4309,7 @@ function parseOpencodeEvents(events) {
4308
4309
  }
4309
4310
  const part = partFromEvent(event);
4310
4311
  if (!part) continue;
4311
- let id = `opencode-${event.timestamp}-${messages.length}`;
4312
+ let id = `opencode-${event.type}-${event.timestamp}`;
4312
4313
  if (typeof part.callID === "string") id = `opencode-${part.callID}`;
4313
4314
  else if (typeof part.id === "string") id = `opencode-${part.id}`;
4314
4315
  const time = isRecord(part.time) ? part.time : null;
@@ -5252,38 +5253,81 @@ function stableString(value) {
5252
5253
  return String(value);
5253
5254
  }
5254
5255
  }
5255
- function duplicateMessage(a, b) {
5256
- if (a.id === b.id) return true;
5257
- if (a.type !== b.type || !nearTimestamp(a.timestamp, b.timestamp)) {
5258
- return false;
5256
+ function duplicateKey(message) {
5257
+ if (message.type === "user" || message.type === "agent" || message.type === "reasoning") {
5258
+ return JSON.stringify([message.type, message.content]);
5259
5259
  }
5260
- if (a.type === "user" && b.type === "user") return a.content === b.content;
5261
- if (a.type === "agent" && b.type === "agent") return a.content === b.content;
5262
- if (a.type === "reasoning" && b.type === "reasoning") return a.content === b.content;
5263
- if (a.type === "command" && b.type === "command") return a.command === b.command;
5264
- if (a.type === "web_search" && b.type === "web_search") return a.query === b.query;
5265
- if (a.type === "subagent" && b.type === "subagent") {
5266
- return a.description === b.description && a.prompt === b.prompt && a.subagentType === b.subagentType && a.model === b.model;
5260
+ if (message.type === "command") return JSON.stringify([message.type, message.command]);
5261
+ if (message.type === "web_search") return JSON.stringify([message.type, message.query]);
5262
+ if (message.type === "subagent") {
5263
+ return JSON.stringify([
5264
+ message.type,
5265
+ message.description,
5266
+ message.prompt,
5267
+ message.subagentType,
5268
+ message.model
5269
+ ]);
5267
5270
  }
5268
- if (a.type === "todo_list" && b.type === "todo_list") {
5269
- return stableString(a.items) === stableString(b.items);
5271
+ if (message.type === "todo_list") {
5272
+ return JSON.stringify([message.type, stableString(message.items)]);
5270
5273
  }
5271
- if (a.type === "patch" && b.type === "patch") {
5272
- return stableString(a.operations) === stableString(b.operations);
5274
+ if (message.type === "patch") {
5275
+ return JSON.stringify([message.type, stableString(message.operations)]);
5273
5276
  }
5274
- if (a.type === "tool_call" && b.type === "tool_call") {
5275
- return a.server === b.server && a.tool === b.tool && stableString(a.input) === stableString(b.input);
5277
+ if (message.type === "tool_call") {
5278
+ return JSON.stringify([
5279
+ message.type,
5280
+ message.server,
5281
+ message.tool,
5282
+ stableString(message.input)
5283
+ ]);
5276
5284
  }
5277
- return false;
5285
+ return null;
5286
+ }
5287
+ function duplicateBucketKey(message, bucketOffset = 0) {
5288
+ const key = duplicateKey(message);
5289
+ if (key === null) return null;
5290
+ const bucket = Math.floor(parseTimestampMs(message.timestamp) / DUPLICATE_WINDOW_MS) + bucketOffset;
5291
+ return JSON.stringify([bucket, key]);
5278
5292
  }
5279
5293
  function mergeCodexAspDisplayMessages(primary, supplemental) {
5280
5294
  const merged = [...primary];
5295
+ const firstIndexById = /* @__PURE__ */ new Map();
5296
+ const indexesByDuplicateBucket = /* @__PURE__ */ new Map();
5297
+ const indexMessage = (message, index) => {
5298
+ if (!firstIndexById.has(message.id)) firstIndexById.set(message.id, index);
5299
+ const key = duplicateBucketKey(message);
5300
+ if (key === null) return;
5301
+ const indexes = indexesByDuplicateBucket.get(key) ?? /* @__PURE__ */ new Set();
5302
+ indexes.add(index);
5303
+ indexesByDuplicateBucket.set(key, indexes);
5304
+ };
5305
+ const unindexMessage = (message, index) => {
5306
+ const key = duplicateBucketKey(message);
5307
+ if (key === null) return;
5308
+ const indexes = indexesByDuplicateBucket.get(key);
5309
+ indexes?.delete(index);
5310
+ if (indexes?.size === 0) indexesByDuplicateBucket.delete(key);
5311
+ };
5312
+ merged.forEach(indexMessage);
5281
5313
  for (const message of supplemental) {
5282
- const duplicateIndex = merged.findIndex((existing) => duplicateMessage(existing, message));
5283
- if (duplicateIndex === -1) {
5284
- merged.push(message);
5314
+ let duplicateIndex = firstIndexById.get(message.id);
5315
+ if (duplicateIndex === void 0) {
5316
+ for (let bucketOffset = -1; bucketOffset <= 1; bucketOffset += 1) {
5317
+ const key = duplicateBucketKey(message, bucketOffset);
5318
+ for (const index of (key === null ? void 0 : indexesByDuplicateBucket.get(key)) ?? []) {
5319
+ if (nearTimestamp(merged[index].timestamp, message.timestamp) && (duplicateIndex === void 0 || index < duplicateIndex)) {
5320
+ duplicateIndex = index;
5321
+ }
5322
+ }
5323
+ }
5324
+ }
5325
+ if (duplicateIndex === void 0) {
5326
+ indexMessage(message, merged.push(message) - 1);
5285
5327
  } else if (message.type === "user") {
5328
+ unindexMessage(merged[duplicateIndex], duplicateIndex);
5286
5329
  merged[duplicateIndex] = { ...merged[duplicateIndex], timestamp: message.timestamp };
5330
+ indexMessage(merged[duplicateIndex], duplicateIndex);
5287
5331
  }
5288
5332
  }
5289
5333
  return merged.map((message, index) => ({ message, index })).sort((a, b) => parseTimestampMs(a.message.timestamp) - parseTimestampMs(b.message.timestamp) || a.index - b.index).map(({ message }) => message);
@@ -5539,7 +5583,7 @@ function loadEngineEnv() {
5539
5583
  WORKSPACE_ID: readEnv("WORKSPACE_ID"),
5540
5584
  LINEAR_SESSION_ID: readEnv("LINEAR_SESSION_ID"),
5541
5585
  LINEAR_ACCESS_TOKEN: readEnv("LINEAR_ACCESS_TOKEN"),
5542
- SLACK_BOT_TOKEN: readEnv("SLACK_BOT_TOKEN"),
5586
+ REPLICAS_SLACK_BOT_TOKEN: readEnv("REPLICAS_SLACK_BOT_TOKEN"),
5543
5587
  SLACK_CHANNEL_ID: readEnv("SLACK_CHANNEL_ID"),
5544
5588
  SLACK_THREAD_TS: readEnv("SLACK_THREAD_TS"),
5545
5589
  ANTHROPIC_API_KEY: readEnv("ANTHROPIC_API_KEY"),
@@ -7174,7 +7218,7 @@ var EnvironmentDetailsService = class {
7174
7218
  details.gitlabAccessConfigured = gitlabAccessConfigured;
7175
7219
  details.gitlabCredentialsConfigured = gitlabAccessConfigured;
7176
7220
  details.linearAccessConfigured = Boolean(ENGINE_ENV.LINEAR_SESSION_ID || ENGINE_ENV.LINEAR_ACCESS_TOKEN);
7177
- details.slackAccessConfigured = Boolean(ENGINE_ENV.SLACK_BOT_TOKEN);
7221
+ details.slackAccessConfigured = Boolean(ENGINE_ENV.REPLICAS_SLACK_BOT_TOKEN);
7178
7222
  const freshRepos = repositories.map((repo) => ({
7179
7223
  repositoryName: repo.name,
7180
7224
  warmHookCompleted: "n/a",
@@ -8363,9 +8407,9 @@ async function fetchImageAsBase64(url, linearAccessToken) {
8363
8407
  }
8364
8408
  }
8365
8409
  if (hostname === "files.slack.com") {
8366
- const token = ENGINE_ENV.SLACK_BOT_TOKEN;
8410
+ const token = ENGINE_ENV.REPLICAS_SLACK_BOT_TOKEN;
8367
8411
  if (!token) {
8368
- throw new Error(`Cannot fetch Slack file from ${url}: SLACK_BOT_TOKEN is not configured`);
8412
+ throw new Error(`Cannot fetch Slack file from ${url}: REPLICAS_SLACK_BOT_TOKEN is not configured`);
8369
8413
  }
8370
8414
  headers["Authorization"] = `Bearer ${token}`;
8371
8415
  }
@@ -10732,7 +10776,7 @@ var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
10732
10776
  var MIN_CODEX_CLI_VERSION = "0.144.6";
10733
10777
  var CODEX_UPGRADE_TIMEOUT_MS = 12e4;
10734
10778
  var codexCliVersionEnsured = null;
10735
- var ENGINE_PACKAGE_VERSION = "0.1.567";
10779
+ var ENGINE_PACKAGE_VERSION = "0.1.569";
10736
10780
  var INITIALIZE_METHOD = "initialize";
10737
10781
  var INITIALIZED_NOTIFICATION = "initialized";
10738
10782
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -11027,6 +11071,7 @@ var THREAD_GOAL_SET_METHOD = "thread/goal/set";
11027
11071
  var THREAD_GOAL_GET_METHOD = "thread/goal/get";
11028
11072
  var THREAD_GOAL_CLEAR_METHOD = "thread/goal/clear";
11029
11073
  var THREAD_COMPACT_START_METHOD = "thread/compact/start";
11074
+ var THREAD_INJECT_ITEMS_METHOD = "thread/inject_items";
11030
11075
  var THREAD_SETTINGS_UPDATE_METHOD = "thread/settings/update";
11031
11076
  var TURN_START_METHOD = "turn/start";
11032
11077
  var TURN_INTERRUPT_METHOD = "turn/interrupt";
@@ -11699,6 +11744,10 @@ var DuplicateDefaultChatError = class extends Error {
11699
11744
  // src/managers/codex-asp/codex-asp-manager.ts
11700
11745
  var GOAL_TURN_CONTINUATION_GRACE_MS = 5e3;
11701
11746
  var CODEX_SLASH_COMMANDS_CACHE_MS = 3e4;
11747
+ var POLICY_REASON_INJECTION_TIMEOUT_MS = 5e3;
11748
+ function formatBlockedCommandReason(reason) {
11749
+ return reason?.includes("Blocked by organization policy") ? reason : `Blocked by organization policy: ${reason || "agents may not merge pull requests."}`;
11750
+ }
11702
11751
  async function readCodexAspThreadHistory(threadId) {
11703
11752
  let entries;
11704
11753
  try {
@@ -12454,6 +12503,11 @@ var CodexAspManager = class extends CodingAgentManager {
12454
12503
  serverRequest.params.cwd,
12455
12504
  serverRequest.params.commandActions
12456
12505
  );
12506
+ await this.injectBlockedCommandReason(
12507
+ host,
12508
+ serverRequest.params.threadId,
12509
+ result.reason
12510
+ );
12457
12511
  host.client.respond(requestId, { decision: "decline" });
12458
12512
  return;
12459
12513
  }
@@ -12490,6 +12544,11 @@ var CodexAspManager = class extends CodingAgentManager {
12490
12544
  serverRequest.params.cwd,
12491
12545
  [{ type: "unknown", command }]
12492
12546
  );
12547
+ await this.injectBlockedCommandReason(
12548
+ host,
12549
+ this.currentThreadId ?? serverRequest.params.conversationId,
12550
+ result.reason
12551
+ );
12493
12552
  host.client.respond(requestId, { decision: "denied" });
12494
12553
  return;
12495
12554
  }
@@ -12540,11 +12599,38 @@ var CodexAspManager = class extends CodingAgentManager {
12540
12599
  }
12541
12600
  host.client.reject(requestId, -32601, `Replicas does not support ASP server request ${method}`);
12542
12601
  }
12602
+ async injectBlockedCommandReason(host, threadId, reason) {
12603
+ try {
12604
+ await host.client.request(
12605
+ THREAD_INJECT_ITEMS_METHOD,
12606
+ {
12607
+ threadId,
12608
+ items: [{
12609
+ type: "message",
12610
+ role: "developer",
12611
+ content: [{
12612
+ type: "input_text",
12613
+ text: [
12614
+ "<organization_policy>",
12615
+ "The command currently awaiting approval was rejected for this reason:",
12616
+ formatBlockedCommandReason(reason),
12617
+ "Do not retry the blocked command unchanged. Adjust your approach to comply with this policy.",
12618
+ "</organization_policy>"
12619
+ ].join("\n")
12620
+ }]
12621
+ }]
12622
+ },
12623
+ { timeoutMs: POLICY_REASON_INJECTION_TIMEOUT_MS }
12624
+ );
12625
+ } catch (error) {
12626
+ console.warn("[CodexAspManager] Failed to inject blocked command reason:", error);
12627
+ }
12628
+ }
12543
12629
  nextTranscriptSequence() {
12544
12630
  return this.codexAspSequence++;
12545
12631
  }
12546
12632
  recordBlockedCommand(threadId, turnId, itemId, command, reason, startedAtMs, cwd, commandActions) {
12547
- const output = reason?.includes("Blocked by organization policy") ? reason : `Blocked by organization policy: ${reason || "agents may not merge pull requests."}`;
12633
+ const output = formatBlockedCommandReason(reason);
12548
12634
  const timestamp = timestampFromMilliseconds(startedAtMs);
12549
12635
  const blockedItem = {
12550
12636
  type: "commandExecution",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.567",
3
+ "version": "0.1.569",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",