@op1/threads 0.1.5 → 0.1.6

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/README.md CHANGED
@@ -47,7 +47,7 @@ The native tool names use namespace `threads` and individual names `spawn`, `lis
47
47
 
48
48
  | Tool | Input | Result |
49
49
  | --- | --- | --- |
50
- | `threads_spawn` | `{ key, title, directory, task }` | Worker view |
50
+ | `threads_spawn` | `{ key, title, directory, task, agent? }` | Worker view |
51
51
  | `threads_list` | `{}` | `{ workers: WorkerView[] }` |
52
52
  | `threads_send` | `{ workerID, key, text }` | `{ workerID, messageID }` |
53
53
  | `threads_interrupt` | `{ workerID }` | Worker view |
@@ -56,21 +56,31 @@ The native tool names use namespace `threads` and individual names `spawn`, `lis
56
56
 
57
57
  All fields are strings except `evidence`, which is an array of strings. Verdicts are `PASS`, `PASS WITH NOTES`, `FAIL`, and `INCONCLUSIVE`. Each tool returns JSON in native `content` and the same value in `output`.
58
58
 
59
- `directory` must exist and be absolute. Spawn uses native `subagent` permission gating. The worker uses the coordinator's agent and model, with resolved agent permissions followed by session permissions. Its actual tool actions still pass through native permission checks. There is no separate directory-approval flow or agent/model override.
59
+ `directory` must exist and be absolute. Without `agent`, the worker inherits the coordinator's active agent and resolved model, with agent permissions followed by session permissions.
60
+
61
+ Set `agent` to use a configured profile, such as `agent: "vera-core"` for a VERA workstream or `agent: "vera-auditor-readonly"` for an independent review. The plugin resolves the profile in the assigned directory. OpenCode supplies its system prompt and step limit. The profile's model and variant take precedence; a profile without a model inherits the coordinator's resolved model. Explicit selection supports `primary`, `all`, and `subagent` profiles on OpenCode 2.0.3.
62
+
63
+ Explicit selection requires the caller's ordered agent and session rules to allow `subagent` for that exact agent ID. A matching `deny` or `ask` rejects the request before creation. OpenCode's plugin API cannot request approval for an input-dependent agent ID.
64
+
65
+ A selected worker uses its profile's permissions. Parent session `deny` and `ask` rules at creation become hard denials on the worker, and parent allows are not copied. This conservative rule also drops parent allow exceptions that follow a denial. It prevents inherited permissions from relaxing a read-only profile. Each managed worker receives one explicit grant for `threads_report`, whose handler verifies ownership. Native subagents retain their own profiles and inherit those session restrictions. Omitting `agent` keeps the existing inheritance behavior.
60
66
 
61
67
  Tool identity comes from the calling session. Only the owning coordinator can send, interrupt, or hide a worker. Only the exact original top-level worker can report. Native subagents and managed workers cannot spawn managed workers. Native `subagent` remains available.
62
68
 
63
69
  ## Identity and retries
64
70
 
65
- The coordinator ID and spawn key determine the worker ID. Worker metadata contains `opThreads` with exactly `workerID`, `coordinatorID`, `key`, `fingerprint`, `initialMessageID`, and `reportMessageID`. There is no native `parentID`.
71
+ The coordinator ID and spawn key determine the worker ID. Worker metadata contains `opThreads` with exactly `workerID`, `coordinatorID`, `key`, `fingerprint`, `initialMessageID`, and `reportMessageID`. Explicit-role workers also carry `opThreadsRole: true`. There is no native `parentID`.
72
+
73
+ The first create includes both message IDs. Identical spawn retries reuse the original session and initial message ID, even if the profile configuration has changed. They do not reset its agent or model. Changing the title, directory, task, or explicit agent under that key is an error. Requests that omit `agent` retain their pre-role fingerprint. Startup never replays initial prompts.
66
74
 
67
- The first create includes both message IDs. Subsequent creates adopt the original metadata returned by OpenCode. Identical spawn retries reuse the initial message ID. Changing the title, directory, or task under that key is an error. Startup never replays initial prompts.
75
+ The initial prompt hook resolves a selected profile after OpenCode loads the assigned directory's configuration. It rechecks the caller's role authorization and selects the profile's model before admitting the task. If the profile is unavailable, admission fails without running the task. The indexed worker remains recoverable: fix the profile configuration and retry the identical request. Until initialization completes, managed follow-ups and direct prompts are rejected. Initialization is recorded after native admission; a retry repairs that record if interrupted between the two writes.
68
76
 
69
77
  Send keys are scoped to the worker and determine a stable message ID. A retry with different text is rejected. Each worker has one task and one terminal report. Identical report retries return the original report; conflicting reports are rejected. Send clarifications within the existing task, and use a new spawn key for new work. The persisted report view is keyed by the original report message ID, so recreating a deleted worker cannot inherit an old verdict.
70
78
 
71
79
  ## Worker views and limits
72
80
 
73
- `WorkerView` contains `workerID`, `coordinatorID`, `key`, `title`, `directory`, `outcome`, `report`, and `hidden`. `outcome` is the native last execution outcome, or `null` before one exists. It is not current activity. Native tabs display current busy, attention, and unread state.
81
+ `WorkerView` contains `workerID`, `coordinatorID`, `key`, `title`, `directory`, `agent`, `model`, `outcome`, `report`, and `hidden`. `agent` and `model` reflect the native session's saved selection, or `null` if unset. A model contains `providerID`, `id`, and an optional `variant`.
82
+
83
+ `outcome` is the native last execution outcome, or `null` before one exists. It is not current activity. Native tabs display current busy, attention, and unread state.
74
84
 
75
85
  `report` is the explicit worker claim, or `null`. Native `succeeded` means the agent loop completed, not that the assigned task passed.
76
86
 
@@ -92,7 +102,7 @@ Reports reach the coordinator through silent synthetic messages. They remain ava
92
102
 
93
103
  All open native root-session tabs are automatically grouped by OpenCode project ID, including sessions not managed by this plugin. Projects follow their first appearance in the current tab order. Worktrees with the same project ID stay together.
94
104
 
95
- Within each project, running sessions, the selected tab, and sessions waiting for input come before idle sessions. Tabs with the same priority keep their relative order. Idle is a display priority, not a completion verdict. Each tab with unloaded project metadata stays in its own group until that metadata becomes available. Reconciliation moves only out-of-order tabs, without changing focus or closing and reopening them to reorder. Native tabs do not support divider rows.
105
+ Within each project, running sessions and sessions waiting for input come before idle sessions. Tabs with the same priority keep their relative order. Selecting an idle tab does not reorder it. Native tab order is shared between terminals in the same directory, so sorting by each terminal's selection would make them repeatedly undo each other's moves. Idle is a display priority, not a completion verdict. Each tab with unloaded project metadata stays in its own group until that metadata becomes available. Reconciliation moves only out-of-order tabs, without changing focus or closing and reopening them to reorder. Native tabs do not support divider rows.
96
106
 
97
107
  The RPC definition is `ThreadsRpc` in `src/rpc.ts`, with ID `threads`. `snapshot` is read-only. The user-invoked `restore` method clears hidden state for the supplied coordinators' workers. Both methods accept and return:
98
108
 
@@ -109,6 +119,10 @@ Run `bun run typecheck`, `bun test`, and `bun run verify:live`. The live check r
109
119
 
110
120
  Run `bun run verify:tabs` to verify project grouping across real git worktrees, activity-based ordering, permission prompts, completed and resumed workers, focus preservation, and TUI reopening.
111
121
 
122
+ Run `bun run verify:roles` to verify named profiles against the native server and deterministic model endpoint. It checks actual system prompts, model variants, native delegation, read-only execution, reporting, and role-aware retries.
123
+
124
+ Run `bun run verify:idle-tabs` to open two native terminals on different idle sessions in the same directory and verify that their shared tab order stays stable.
125
+
112
126
  Pass an extracted package directory to test the release artifact: `bun run verify:live /absolute/path/to/package`.
113
127
 
114
128
  The native session and plugin index are separate writes. A crash after session creation but before indexing requires an explicit identical spawn retry. A crash after native report admission but before saving the report view requires an explicit report retry; the native coordinator notification remains canonical and is not duplicated. There is no custom outbox or startup task replay.
package/index.ts CHANGED
@@ -15,6 +15,11 @@ export default Plugin.define({
15
15
  .default(4)
16
16
  .parse(ctx.options.maxWorkers);
17
17
  const workers = threads(ctx, limit);
18
+ await ctx.session.hook("prompt", (event) => {
19
+ return workers.preparePrompt(
20
+ event.sessionID, event.messageID, event.metadata?.opThreadsCallerAgent,
21
+ );
22
+ });
18
23
  const models = new Map<
19
24
  SessionContext["sessionID"],
20
25
  SessionContext["model"]
@@ -38,7 +43,7 @@ export default Plugin.define({
38
43
  editor.add({
39
44
  name: "spawn",
40
45
  description:
41
- "Delegate a task to a top-level worker in an existing absolute directory. Both you and the worker may use native subagent when useful. Include any delegation limits in task. Workers cannot call threads_spawn. Reuse key only for identical requests.",
46
+ "Delegate a task to a top-level worker in an existing absolute directory. Set agent to a configured profile (for example vera-core); its prompt, model preference, and permissions apply. Omit agent to inherit your active agent and model. Native subagent remains available within the selected profile's permissions and task's delegation limits. Workers cannot call threads_spawn. Reuse key only for identical requests, including agent.",
42
47
  input: Spawn,
43
48
  output: WorkerView,
44
49
  options: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@op1/threads",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Visible top-level worker sessions for OpenCode V2, with native tabs and durable reports.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -25,6 +25,8 @@
25
25
  "typecheck": "tsc --noEmit",
26
26
  "test": "bun test",
27
27
  "verify:live": "uv run --with pyte python scripts/verify-live.py",
28
+ "verify:roles": "python3 scripts/verify-roles.py",
29
+ "verify:idle-tabs": "uv run --with pyte python scripts/verify-idle-tabs.py",
28
30
  "verify:tabs": "uv run --with pyte python scripts/verify-tab-groups.py"
29
31
  },
30
32
  "dependencies": {
@@ -16,10 +16,15 @@ Both the parent and managed workers may use native `subagent` when useful. Deleg
16
16
 
17
17
  1. Confirm the `threads_spawn` tool is available. If it is unavailable, use native `subagent` or report that the plugin needs activation. Do not substitute a hidden `opencode run` process.
18
18
  2. Assign an existing absolute directory and a stable task key.
19
- 3. Call `threads_spawn` with `key`, `title`, `directory`, and `task`. Include the goal, scope, relevant context, constraints, delegation allowance, acceptance criteria, verification commands, and expected report in `task`.
20
- 4. Save the returned worker session ID with the work unit.
19
+ 3. If the tool schema supports `agent`, select a configured profile explicitly. Use `agent: "vera-core"` for a VERA workstream that owns integration and may delegate. Use a specialist profile for a leaf task or read-only review. Omitting `agent` inherits the caller's active agent and model. Older plugin versions always inherit them.
20
+ 4. Call `threads_spawn` with `key`, `title`, `directory`, `task`, and the selected `agent`. Include the goal, scope, relevant context, constraints, delegation allowance, acceptance criteria, verification commands, and expected report in `task`.
21
+ 5. Save the returned worker session ID and inspect its returned agent and model when present.
21
22
 
22
- An identical spawn key retries the original admission. Different work requires a new key. A worker inherits the coordinator's agent, model, and permission constraints. It has a separate conversation, so include all context it needs in the task brief.
23
+ An identical spawn key retries the original admission. Different work or a different agent requires a new key. A selected profile supplies its real system prompt, model preference, and permissions in the assigned directory. A profile without a model inherits the caller's resolved model. It has a separate conversation, so include all context it needs in the task brief. Mentioning a role in the brief does not select that profile.
24
+
25
+ Explicit role selection requires an `allow` for `subagent` on that role ID. Selected workers carry parent session `deny` and `ask` rules as hard denials; parent allow exceptions do not reopen them. Read-only workers can still call their ownership-checked `threads_report` tool.
26
+
27
+ If the selected profile is unavailable, fix its configuration and retry the identical spawn request. The indexed worker stays uninitialized until that retry succeeds. Do not use `threads_send` to start it.
23
28
 
24
29
  ## Write the delegation allowance
25
30
 
@@ -27,7 +32,7 @@ The parent writes `task`; the plugin appends worker instructions. Give workers t
27
32
 
28
33
  > You may work directly or use native `subagent` for bounded tasks and reviews when useful. Pass your scope and constraints to subagents, review their results, and resolve outstanding work before calling `threads_report` yourself.
29
34
 
30
- Use a no-delegation restriction only for a task-specific reason or an explicit user constraint, and state the reason. Being a managed worker or having bounded scope does not by itself make the worker a leaf. To limit managed-thread nesting, say `Do not call threads_spawn` rather than `No children`.
35
+ Honor the selected profile's restrictions. A specialist that denies native delegation stays a leaf, and a read-only profile stays read-only. For a delegation-capable profile, add a no-delegation restriction only for a task-specific reason or an explicit user constraint, and state the reason. Being a managed worker or having bounded scope does not by itself make the worker a leaf. To limit managed-thread nesting, say `Do not call threads_spawn` rather than `No children`.
31
36
 
32
37
  ## Coordinate
33
38
 
@@ -0,0 +1,32 @@
1
+ import type { Permission } from "@opencode/schema/permission";
2
+
3
+ function matches(pattern: string, value: string) {
4
+ const expression = pattern
5
+ .replaceAll("\\", "/")
6
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
7
+ .replaceAll("*", ".*")
8
+ .replaceAll("?", ".");
9
+ const normalized = value.replaceAll("\\", "/");
10
+ const match = new RegExp(
11
+ `^${expression}$`,
12
+ process.platform === "win32" ? "is" : "s",
13
+ ).exec(normalized);
14
+ return match?.[0] === normalized;
15
+ }
16
+
17
+ export function delegationEffect(
18
+ rules: Permission.Ruleset,
19
+ agentID: string,
20
+ ): Permission.Effect {
21
+ return rules.findLast((rule) =>
22
+ matches(rule.action, "subagent") && matches(rule.resource, agentID)
23
+ )?.effect ?? "ask";
24
+ }
25
+
26
+ export function requireDelegation(rules: Permission.Ruleset, agentID: string) {
27
+ if (delegationEffect(rules, agentID) !== "allow") {
28
+ throw new Error(
29
+ `Spawning agent "${agentID}" requires an explicit subagent allow for that agent ID; deny or ask cannot authorize managed spawning.`,
30
+ );
31
+ }
32
+ }
package/src/rpc.ts CHANGED
@@ -15,6 +15,12 @@ export const WorkerView = z.object({
15
15
  key: z.string(),
16
16
  title: z.string(),
17
17
  directory: z.string(),
18
+ agent: z.string().nullable(),
19
+ model: z.object({
20
+ providerID: z.string(),
21
+ id: z.string(),
22
+ variant: z.string().optional(),
23
+ }).nullable(),
18
24
  outcome: z.enum(["succeeded", "failed", "interrupted"]).nullable(),
19
25
  report: Report.nullable(),
20
26
  hidden: z.boolean(),
package/src/threads.ts CHANGED
@@ -4,9 +4,11 @@ import { isAbsolute } from "node:path";
4
4
  import type { Plugin } from "@opencode/plugin";
5
5
  import type { SessionContext } from "@opencode/plugin/promise/session";
6
6
  import type { ToolContext } from "@opencode/plugin/promise/tool";
7
+ import type { Permission } from "@opencode/schema/permission";
7
8
  import { Session } from "@opencode/schema/session";
8
9
  import { SessionMessage } from "@opencode/schema/session-message";
9
10
  import { z } from "zod";
11
+ import { requireDelegation } from "./permissions";
10
12
  import { Report, WorkerView } from "./rpc";
11
13
 
12
14
  const sessionID = z.string().transform((value) => Session.ID.make(value));
@@ -33,6 +35,9 @@ export const Spawn = z
33
35
  title: z.string().min(1),
34
36
  directory: z.string().min(1),
35
37
  task: z.string().min(1),
38
+ agent: z.string().min(1).optional().describe(
39
+ "Configured agent ID in the worker's directory. Omit to inherit the caller's agent and model.",
40
+ ),
36
41
  })
37
42
  .strict();
38
43
  export const Send = z
@@ -49,7 +54,12 @@ const digest = (parts: string[]) =>
49
54
  export const workerIdentity = (coordinatorID: string, key: string) =>
50
55
  Session.ID.make(`ses_${digest([coordinatorID, key]).slice(0, 32)}`);
51
56
  export const fingerprint = (input: z.infer<typeof Spawn>) =>
52
- digest([input.title, input.directory, input.task]);
57
+ digest([
58
+ input.title,
59
+ input.directory,
60
+ input.task,
61
+ ...(input.agent === undefined ? [] : [input.agent]),
62
+ ]);
53
63
 
54
64
  const locks = new Map<string, Promise<void>>();
55
65
  export async function serialized<T>(
@@ -96,6 +106,18 @@ export function threads(
96
106
  `reports/${link.workerID}/${link.reportMessageID}`;
97
107
  const visibilityKey = (link: z.infer<typeof Link>) =>
98
108
  `visibility/${link.workerID}/${link.reportMessageID}`;
109
+ const initializedKey = (link: z.infer<typeof Link>) =>
110
+ `initialized/${link.workerID}/${link.initialMessageID}`;
111
+
112
+ async function initialized(session: NativeSession, link: z.infer<typeof Link>) {
113
+ return session.metadata?.opThreadsRole !== true ||
114
+ await ctx.storage.get(initializedKey(link)) === true;
115
+ }
116
+
117
+ async function callerPermissions(session: NativeSession, agentID: string) {
118
+ const agent = await ctx.agent.get({ agentID, location: session.location });
119
+ return [...agent.data.permissions, ...(session.permissions ?? [])];
120
+ }
99
121
 
100
122
  async function view(
101
123
  session: NativeSession,
@@ -110,6 +132,8 @@ export function threads(
110
132
  key: link.key,
111
133
  title: session.title ?? link.key,
112
134
  directory: session.location.directory,
135
+ agent: session.agent ?? null,
136
+ model: session.model ?? null,
113
137
  outcome: session.outcome ?? null,
114
138
  report,
115
139
  hidden:
@@ -141,6 +165,7 @@ export function threads(
141
165
  throw error;
142
166
  await ctx.storage.remove(reportKey(link));
143
167
  await ctx.storage.remove(visibilityKey(link));
168
+ await ctx.storage.remove(initializedKey(link));
144
169
  await ctx.storage.remove(entry.key);
145
170
  continue;
146
171
  }
@@ -161,6 +186,34 @@ export function threads(
161
186
 
162
187
  return {
163
188
  list,
189
+ async preparePrompt(actor: string, messageID: string, callerAgent: unknown) {
190
+ const session = await ctx.session.get({ sessionID: actor });
191
+ if (session.metadata?.opThreadsRole !== true) return;
192
+ const recorded = Link.parse(session.metadata.opThreads);
193
+ if (session.parentID !== undefined || recorded.workerID !== session.id) return;
194
+ const link = workerLink(session);
195
+ if (await initialized(session, link)) return;
196
+ if (link.initialMessageID !== messageID || session.agent === undefined) {
197
+ throw new Error(
198
+ "Worker initialization is pending. Retry the original threads_spawn request.",
199
+ );
200
+ }
201
+ const coordinator = await ctx.session.get({ sessionID: link.coordinatorID });
202
+ requireDelegation(
203
+ await callerPermissions(coordinator, z.string().min(1).parse(callerAgent)),
204
+ session.agent,
205
+ );
206
+ const agent = await ctx.agent.get({
207
+ agentID: session.agent,
208
+ location: session.location,
209
+ });
210
+ if (agent.data.model) {
211
+ await ctx.session.switchModel({
212
+ sessionID: session.id,
213
+ model: agent.data.model,
214
+ });
215
+ }
216
+ },
164
217
  async hide(actor: string, input: z.infer<typeof WorkerTarget>) {
165
218
  const { session, link } = await owned(actor, input.workerID);
166
219
  await ctx.storage.set(visibilityKey(link), true);
@@ -199,9 +252,17 @@ export function threads(
199
252
  throw new Error("directory must be an existing absolute directory");
200
253
  }
201
254
  const workerID = workerIdentity(actor, input.key);
255
+ let session = await ctx.session.get({ sessionID: workerID }).catch(
256
+ (error: unknown) => {
257
+ const missing = MissingSession.safeParse(error);
258
+ if (!missing.success || missing.data.sessionID !== workerID)
259
+ throw error;
260
+ return undefined;
261
+ },
262
+ );
202
263
  const existing = await list(actor);
203
264
  if (
204
- !existing.some((worker) => worker.workerID === workerID) &&
265
+ !session &&
205
266
  existing.filter(
206
267
  (worker) =>
207
268
  !worker.report &&
@@ -211,10 +272,6 @@ export function threads(
211
272
  ) {
212
273
  throw new Error(`Coordinator worker limit reached (${limit})`);
213
274
  }
214
- const agent = await ctx.agent.get({
215
- agentID: runtime.agent,
216
- location: coordinator.location,
217
- });
218
275
  const proposed = Link.parse({
219
276
  workerID,
220
277
  coordinatorID: actor,
@@ -223,18 +280,31 @@ export function threads(
223
280
  initialMessageID: SessionMessage.ID.create(),
224
281
  reportMessageID: SessionMessage.ID.create(),
225
282
  });
226
- const session = await ctx.session.create({
227
- id: workerID,
228
- title: input.title,
229
- location: { directory: input.directory },
230
- agent: runtime.agent,
231
- model: runtime.model,
232
- permissions: [
233
- ...agent.data.permissions,
234
- ...(coordinator.permissions ?? []),
235
- ],
236
- metadata: { opThreads: proposed },
237
- });
283
+ if (!session) {
284
+ const inherited = await callerPermissions(coordinator, runtime.agent);
285
+ if (input.agent !== undefined) requireDelegation(inherited, input.agent);
286
+ session = await ctx.session.create({
287
+ id: workerID,
288
+ title: input.title,
289
+ location: { directory: input.directory },
290
+ agent: input.agent ?? runtime.agent,
291
+ model: runtime.model,
292
+ permissions: [
293
+ ...(input.agent === undefined
294
+ ? inherited
295
+ : (coordinator.permissions ?? [])
296
+ .filter((rule) => rule.effect !== "allow")
297
+ .map((rule) => (
298
+ { ...rule, effect: "deny" } satisfies Permission.Rule
299
+ ))),
300
+ { action: "threads_report", resource: "*", effect: "allow" },
301
+ ],
302
+ metadata: {
303
+ opThreads: proposed,
304
+ ...(input.agent === undefined ? {} : { opThreadsRole: true }),
305
+ },
306
+ });
307
+ }
238
308
  const link = workerLink(session);
239
309
  if (
240
310
  link.coordinatorID !== actor ||
@@ -249,13 +319,22 @@ export function threads(
249
319
  sessionID: link.workerID,
250
320
  id: link.initialMessageID,
251
321
  delivery: "queue",
252
- text: `${input.task}\n\nYou are a managed worker assigned to ${input.directory}. Work only within the assigned scope. You may use native subagent for bounded tasks or reviews when useful, within the brief's delegation limits and inherited permissions. Delegation is optional. Pass relevant context, scope, and constraints to each subagent. Do not call threads_spawn. Review your subagents' results and resolve any outstanding work before reporting. Only you call threads_report with the combined verdict, summary, and evidence; subagents return results to you. Runtime completion alone does not establish task success.`,
322
+ metadata: input.agent === undefined
323
+ ? undefined
324
+ : { opThreadsCallerAgent: runtime.agent },
325
+ text: `${input.task}\n\nYou are a managed worker assigned to ${input.directory}. Work only within the assigned scope. You may use native subagent for bounded tasks or reviews when useful, within the brief's delegation limits and your permissions. Delegation is optional. Pass relevant context, scope, and constraints to each subagent. Do not call threads_spawn. Review your subagents' results and resolve any outstanding work before reporting. Only you call threads_report with the combined verdict, summary, and evidence; subagents return results to you. Runtime completion alone does not establish task success.`,
253
326
  });
327
+ if (input.agent !== undefined) {
328
+ await ctx.storage.set(initializedKey(link), true);
329
+ }
254
330
  return view(await ctx.session.get({ sessionID: workerID }));
255
331
  });
256
332
  },
257
333
  async send(actor: string, input: z.infer<typeof Send>) {
258
- const { link } = await owned(actor, input.workerID);
334
+ const { session, link } = await owned(actor, input.workerID);
335
+ if (!await initialized(session, link)) {
336
+ throw new Error("Worker initialization is pending. Retry the original threads_spawn request.");
337
+ }
259
338
  const id = SessionMessage.ID.make(
260
339
  `msg_${digest([input.workerID, "send", input.key]).slice(0, 32)}`,
261
340
  );
package/tui.ts CHANGED
@@ -22,7 +22,7 @@ export default Plugin.define({
22
22
  const projectID = ctx.data.session.get(tab.sessionID)?.projectID;
23
23
  return {
24
24
  sessionID: tab.sessionID,
25
- priority: tab.busy || tab.active || tab.attention,
25
+ priority: tab.busy || tab.attention,
26
26
  projectID:
27
27
  typeof projectID === "string" && projectID.length > 0
28
28
  ? projectID