openmausbot 0.1.79 → 0.1.80

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.
@@ -0,0 +1,20 @@
1
+ // The model's window, when the driver did not say. A conservative pattern
2
+ // table over the model id: a wrong window would put a wrong percentage next
3
+ // to the context figure, so unknown models get no window and the chip shows
4
+ // the absolute count alone.
5
+ export function modelContextWindow(model) {
6
+ if (!model)
7
+ return undefined;
8
+ const id = model.toLowerCase();
9
+ if (/claude/.test(id) || /^(opus|sonnet|haiku|fable)\b/.test(id))
10
+ return /\[1m\]|-1m\b|1m-context/.test(id) ? 1_000_000 : 200_000;
11
+ if (/^(gpt-5|o[34]\b|codex)/.test(id))
12
+ return 272_000;
13
+ if (id.startsWith("gpt-4.1"))
14
+ return 1_000_000;
15
+ if (/gemini/.test(id))
16
+ return 1_000_000;
17
+ if (id.startsWith("grok"))
18
+ return 256_000;
19
+ return undefined;
20
+ }
@@ -34,6 +34,7 @@ const mask = (value) => (REDACTION_MARKER.test(value) ? value : `«redacted ${va
34
34
  // generic "long hex/base64" heuristic would rewrite real code in the
35
35
  // transcript, so only shapes that are unmistakably credentials match.
36
36
  const KEY_PREFIXES = [
37
+ /\bom[dg]_[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-])/g, // desktop device and model-only credentials
37
38
  /\bsk-(?:ant-|proj-|live-|test-)?[A-Za-z0-9_-]{16,}/g, // anthropic / openai / stripe
38
39
  /\bxai-[A-Za-z0-9_-]{20,}/g, // xai (grok)
39
40
  /\bgsk_[A-Za-z0-9]{40,}/g, // groq
@@ -12,7 +12,7 @@ const nodeSchema = z.object({
12
12
  approvalGranted: z.boolean().default(false),
13
13
  kind: z.enum(["work", "assignment"]).default("work"),
14
14
  });
15
- export const ROOM_HANDOFF_LIMITS = { depth: 4, requests: 24, executions: 48, lifetimeMs: 30 * 60_000, minRunwayMs: 10 * 60_000 };
15
+ export const ROOM_HANDOFF_LIMITS = { depth: 4, requests: 24, executions: 48, lifetimeMs: 30 * 60_000, minRunwayMs: 10 * 60_000, queueMs: 60 * 60_000 };
16
16
  /** Renders elapsed milliseconds as whole minutes, or seconds under one minute. */
17
17
  const duration = (ms) => ms >= 60_000 ? `${Math.floor(ms / 60_000)}m` : `${Math.floor(ms / 1000)}s`;
18
18
  const terminal = (n) => ["completed", "failed", "cancelled"].includes(n.status);
@@ -68,10 +68,14 @@ export class RoomHandoffs {
68
68
  return path;
69
69
  }
70
70
  /** The earliest moment this node may be failed for lifetime: the tree
71
- * ceiling or, for a running node, its own start plus a minimum runway. */
71
+ * ceiling, a running node's own start plus a minimum runway, or, for work
72
+ * parked in a busy teammate's queue, its own queue window (#1238). */
72
73
  deadline(n) {
73
74
  const anchor = n.status === "running" ? n.startedAt ?? n.createdAt : n.createdAt;
74
- return Math.max(this.root(n).createdAt + ROOM_HANDOFF_LIMITS.lifetimeMs, anchor + ROOM_HANDOFF_LIMITS.minRunwayMs);
75
+ const ceiling = n.status === "queued" && n.executions === 0
76
+ ? anchor + ROOM_HANDOFF_LIMITS.queueMs
77
+ : this.root(n).createdAt + ROOM_HANDOFF_LIMITS.lifetimeMs;
78
+ return Math.max(ceiling, anchor + ROOM_HANDOFF_LIMITS.minRunwayMs);
75
79
  }
76
80
  /** An ancestor past its ceiling is not failed while a descendant is still
77
81
  * running inside its own runway; cancelling would cascade into that work. */
@@ -79,17 +83,23 @@ export class RoomHandoffs {
79
83
  return this.children(n.id).some(c => !terminal(c) && ((c.status === "running" && this.now() <= this.deadline(c)) || this.protectsRunner(c)));
80
84
  }
81
85
  /** A parent still owes the follow-up execution that decides on its
82
- * children's results; the ceiling defers to that execution's own runway. */
86
+ * children's results; the ceiling defers to that execution's own runway.
87
+ * A child parked in a queue has produced nothing to decide on yet. */
83
88
  owesFollowUp(n) {
84
89
  if (n.status === "resume")
85
90
  return true;
86
91
  if (n.status !== "waiting")
87
92
  return false;
88
93
  const children = this.children(n.id);
89
- return (children.length > 0 && children.every(c => terminal(c))) || children.some(c => this.owesFollowUp(c));
94
+ return (children.length > 0 && children.every(c => terminal(c))) ||
95
+ children.some(c => this.owesFollowUp(c) || (c.status === "queued" && c.executions === 0));
90
96
  }
91
- /** Names the budget, the node's status, and the elapsed tree time. */
97
+ /** Names the budget, the node's status, and the elapsed time. Work that
98
+ * never started reports the queue window it waited out, not the tree's. */
92
99
  lifetimeError(n) {
100
+ if (n.status === "queued" && n.executions === 0) {
101
+ return `Room handoff queue budget exhausted: never started while waiting for a busy teammate after ${duration(this.now() - n.createdAt)} of the ${duration(ROOM_HANDOFF_LIMITS.queueMs)} queue window`;
102
+ }
93
103
  return `Room handoff lifetime budget exhausted: node was ${n.status} after ${duration(this.now() - this.root(n).createdAt)} of the ${duration(ROOM_HANDOFF_LIMITS.lifetimeMs)} tree lifetime`;
94
104
  }
95
105
  enqueue(source, generation, parentId, target, key, text, approvalGranted = false, rework = false, sourceText = "") {
@@ -26,7 +26,7 @@ export function isProjectEmoji(value) {
26
26
  }
27
27
  const TASK_PATCH_FIELDS = [
28
28
  "title", "projectId", "modelSelection", "approvalMode", "autoApprove", "alwaysAllow",
29
- "unread", "rewound", "pinnedMessageId", "resumeCursors", "lastInstanceId", "cwd",
29
+ "unread", "rewound", "archivedAt", "pinnedMessageId", "resumeCursors", "lastInstanceId", "cwd",
30
30
  "routineRunId",
31
31
  ];
32
32
  /** Everything the BOT authored is scrubbed of content-shaped secrets before
@@ -1485,12 +1485,24 @@ export class Store {
1485
1485
  const turnInput = clean(turn.input);
1486
1486
  const nextCachedInput = Math.min(clean(prev.cachedInput), prevInput)
1487
1487
  + Math.min(clean(turn.cachedInput), turnInput);
1488
+ const contextTokens = clean(turn.context?.tokens);
1489
+ const contextWindow = clean(turn.context?.window);
1488
1490
  task.usage = {
1489
1491
  input: prevInput + turnInput,
1490
1492
  output: prev.output + clean(turn.output),
1491
1493
  ...(cachedKnown ? { cachedInput: nextCachedInput } : {}),
1492
1494
  costUsd: cost === null ? prevCost : (prevCost ?? 0) + cost,
1493
1495
  turns: prev.turns + 1,
1496
+ lastTurn: {
1497
+ input: turnInput, output: clean(turn.output),
1498
+ ...(typeof turn.cachedInput === "number" ? { cachedInput: Math.min(clean(turn.cachedInput), turnInput) } : {}),
1499
+ costUsd: cost,
1500
+ },
1501
+ // a turn that reported no context keeps the previous reading rather
1502
+ // than pretending the window emptied
1503
+ ...(contextTokens > 0
1504
+ ? { context: { tokens: contextTokens, ...(contextWindow > 0 ? { window: contextWindow } : {}) } }
1505
+ : prev.context ? { context: prev.context } : {}),
1494
1506
  };
1495
1507
  this.saveBots();
1496
1508
  this.emit({ type: "bot", botId });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmausbot",
3
- "version": "0.1.79",
3
+ "version": "0.1.80",
4
4
  "description": "Run the OpenMausBot server anywhere and pair your devices to it",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",