@stage-labs/metro 0.1.0-beta.272 → 0.1.0-beta.274
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/marketplace/plugin/STAGE.md +3 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +1 -0
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/runtime/sdk-runner/src/runner.ts +31 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.274",
|
|
4
4
|
"description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -45,6 +45,9 @@ The separate `metadata.status` and `metadata.priority` fields are optional. Stat
|
|
|
45
45
|
- Delete one of your own messages: `delete {"line": "…", "message_id": "…"}`. XMTP has no edit and no typing.
|
|
46
46
|
- History: `read {"line": "…", "limit": 20}`.
|
|
47
47
|
- Poll: `ask {"line": "…", "question": "Ship today?", "options": ["Yes", "No"]}`.
|
|
48
|
+
- Poll votes are numeric reactions with zero-based indexes: `0` is the first option, `1` is the second, and so on, in the poll's stored option order. For the poll above, `0` means Yes and `1` means No. Never use a one-based guess. Match the inbound reaction's `target_id` to the poll's `messageId` returned by `ask`, not its `pollId` or the reaction's own `message_id`.
|
|
49
|
+
- With several questions, the first question still uses bare option indexes (`0`, `1`, …); later questions use `q:o`, with both indexes zero-based. `1:0` means the first option of the second question. Explicit `0:0` also means the first option of the first question.
|
|
50
|
+
- Keep the poll's questions, options and `messageId`: `read` shows `[poll]` and `[reaction]`, not their choices or vote values. If the original mapping is unavailable, do not guess.
|
|
48
51
|
- File: `create_upload {"name": "report.pdf"}`, run the `curl` line it returns in a shell, then `send {"line": "…", "attachments": [{"upload": "<upload_id>"}]}`. Files that are not images are refused over about 190 KiB.
|
|
49
52
|
|
|
50
53
|
**Frames.** A frame is a small interactive view: a report, a choice, a form. Stage shows the frame itself in the chat (its `start` screen when it has screens), at image size, at most 400 x 400, clipped with a fade when taller. A tap opens it full screen. Its buttons do nothing in the chat: they work in the full view.
|
|
@@ -160,6 +160,7 @@ const PRIOR_STAGE: ReadonlySet<string> = new Set([
|
|
|
160
160
|
'f87e9494ea6151d140b742c1b88b4f435ae653559be641fddc2160cd24dd4e1b',
|
|
161
161
|
'cb4307b016855c1a09d48bc8b3924a3080bd78b205963df3d6f20401d0486df9',
|
|
162
162
|
'f2be51c5290d2e61b47f10c379fa45d051a59a6b3f06d99174c2a170074bc8f5',
|
|
163
|
+
'4c727aa110c9023371cfb364f3304ad9db22cdf2501208ef1e42e6ccc245b00f',
|
|
163
164
|
]);
|
|
164
165
|
const PRIOR_MEMORY: ReadonlySet<string> = new Set(['08795399635c8cdec2b50cb4e336f89ae880f3997ae52560ef647a9cdc8f1b08']);
|
|
165
166
|
|
package/runtime/runtime.json
CHANGED
|
@@ -2,6 +2,7 @@ import { query, type CanUseTool, type Options, type Query, type SDKMessage, type
|
|
|
2
2
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
3
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
4
4
|
import type { Activity } from './activity.js';
|
|
5
|
+
import { ActivityTasks } from './activity-tasks.js';
|
|
5
6
|
import { RunnerCalls } from './calls.js';
|
|
6
7
|
import { channelText, type ChannelEvent } from './channel-text.js';
|
|
7
8
|
import type { RunnerConfig } from './config.js';
|
|
@@ -14,6 +15,7 @@ import { METRO_SERVER, type MetroTools } from './tool-proxy.js';
|
|
|
14
15
|
|
|
15
16
|
const STARTUP_WAIT = 'CLAUDE_CODE_MCP_STARTUP_WAIT_MS';
|
|
16
17
|
export const COMPACT_AT = 120_000;
|
|
18
|
+
export const COMPACT_IDLE_MS = 60_000;
|
|
17
19
|
|
|
18
20
|
const MODEL_PINS: ReadonlySet<string> = new Set(['ANTHROPIC_MODEL', 'CLAUDE_CODE_SUBAGENT_MODEL']);
|
|
19
21
|
|
|
@@ -85,6 +87,8 @@ export class Runner {
|
|
|
85
87
|
readonly inbox: Inbox;
|
|
86
88
|
readonly calls: RunnerCalls;
|
|
87
89
|
private readonly watch: SessionWatch;
|
|
90
|
+
private readonly tasks = new ActivityTasks(() => undefined);
|
|
91
|
+
private compactTimer: ReturnType<typeof setTimeout> | null = null;
|
|
88
92
|
private session: Query | null = null;
|
|
89
93
|
private sessionId: string | null = null;
|
|
90
94
|
private compactAsked: Uuid | null = null;
|
|
@@ -101,7 +105,7 @@ export class Runner {
|
|
|
101
105
|
this.inbox = new Inbox((unanswered) => { this.keep(unanswered); }, {
|
|
102
106
|
ready: () => this.watch.admitting,
|
|
103
107
|
dispatch: (message) => { this.watch.dispatched(message.uuid); },
|
|
104
|
-
input: (input) => { parts.activity?.input(input); },
|
|
108
|
+
input: (input) => { this.cancelCompact(); parts.activity?.input(input); },
|
|
105
109
|
queue: (count, oldest) => { parts.activity?.pending(count, oldest); },
|
|
106
110
|
});
|
|
107
111
|
this.calls = new RunnerCalls(this.inbox, parts.activity);
|
|
@@ -133,15 +137,19 @@ export class Runner {
|
|
|
133
137
|
if (this.session === null) throw new Error('the runner was not started');
|
|
134
138
|
try {
|
|
135
139
|
for await (const message of this.session) {
|
|
140
|
+
this.cancelCompact();
|
|
141
|
+
if (message.type === 'system') this.tasks.observe(message);
|
|
136
142
|
const m: Record<string, unknown> = { ...message };
|
|
137
143
|
this.trackInput(m);
|
|
138
144
|
this.watch.observe(m);
|
|
139
145
|
this.note(m);
|
|
140
146
|
this.parts.activity?.observe(message);
|
|
141
147
|
this.inbox.notify();
|
|
148
|
+
this.maybeCompact();
|
|
142
149
|
observe?.(message);
|
|
143
150
|
}
|
|
144
151
|
} finally {
|
|
152
|
+
this.cancelCompact();
|
|
145
153
|
this.ended = true;
|
|
146
154
|
}
|
|
147
155
|
}
|
|
@@ -163,10 +171,12 @@ export class Runner {
|
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
close(cancelActive = true): void {
|
|
174
|
+
this.cancelCompact();
|
|
166
175
|
this.calls.close();
|
|
167
176
|
try {
|
|
168
177
|
if (cancelActive && !this.ended) this.inbox.finished();
|
|
169
178
|
} finally {
|
|
179
|
+
this.ended = true;
|
|
170
180
|
this.inbox.close();
|
|
171
181
|
this.session?.close();
|
|
172
182
|
}
|
|
@@ -218,7 +228,6 @@ export class Runner {
|
|
|
218
228
|
if (m.type === 'result') this.turnDone(m);
|
|
219
229
|
if (m.type !== 'system') return;
|
|
220
230
|
if (m.subtype === 'init') this.started(m);
|
|
221
|
-
else if (m.subtype === 'session_state_changed' && m.state === 'idle') this.maybeCompact();
|
|
222
231
|
else if (m.subtype === 'compact_boundary') {
|
|
223
232
|
this.compacted = true;
|
|
224
233
|
this.compactFloor = null;
|
|
@@ -239,16 +248,33 @@ export class Runner {
|
|
|
239
248
|
this.compactFloor = context;
|
|
240
249
|
this.compacted = false;
|
|
241
250
|
}
|
|
242
|
-
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private compactReady(resuming = false): boolean {
|
|
254
|
+
const busy = this.ended || this.compactAsked !== null || !this.watch.safe || !this.watch.admitting || (!resuming && (this.calls.live || this.inbox.pending > 0 || this.tasks.running > 0));
|
|
255
|
+
return compactDue({ context: this.watch.context, floor: this.compactFloor, limit: this.parts.compactAt ?? COMPACT_AT, busy });
|
|
243
256
|
}
|
|
244
257
|
|
|
245
258
|
private maybeCompact(resuming = false): void {
|
|
246
|
-
|
|
247
|
-
if (
|
|
259
|
+
if (!this.compactReady(resuming)) return;
|
|
260
|
+
if (resuming) { this.askCompact(true); return; }
|
|
261
|
+
this.compactTimer = setTimeout(() => {
|
|
262
|
+
this.compactTimer = null;
|
|
263
|
+
if (this.compactReady()) this.askCompact(false);
|
|
264
|
+
}, COMPACT_IDLE_MS);
|
|
265
|
+
this.compactTimer.unref();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private askCompact(resuming: boolean): void {
|
|
248
269
|
this.compactAsked = this.inbox.compact();
|
|
249
270
|
log.info({ context: this.watch.context, resuming }, 'sdk-runner: compacting at a safe idle boundary');
|
|
250
271
|
}
|
|
251
272
|
|
|
273
|
+
private cancelCompact(): void {
|
|
274
|
+
if (this.compactTimer !== null) clearTimeout(this.compactTimer);
|
|
275
|
+
this.compactTimer = null;
|
|
276
|
+
}
|
|
277
|
+
|
|
252
278
|
private started(m: Record<string, unknown>): void {
|
|
253
279
|
const id = typeof m.session_id === 'string' ? m.session_id : null;
|
|
254
280
|
if (id === null || id === this.sessionId) return;
|