pi-landstrip 0.20.3 → 0.20.5

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.ts +116 -176
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -65,7 +65,7 @@ and `/landstrip-subagents` for subagent/task management (`Tasks`, `Log`, `Subage
65
65
  `Primary`, `Settings`, `Help`). Task IDs accept unique prefixes; bare IDs open logs.
66
66
  `/landstrip-subagents tasks kill <id>` terminates a task. `Ctrl+Shift+A` cycles
67
67
  primary agents while idle. The `task` tool accepts `background: true`, `include_context: true`,
68
- and `task_id` to continue a saved task; the `tasks` tool supports `list`, `inspect`, `wait`,
68
+ and `task_id` to continue a saved task; the `tasks` tool supports `list`, `inspect`,
69
69
  `steer`, and `cancel`. Press `S` in `Tasks` or `Enter` in running/queued logs to steer workers.
70
70
 
71
71
  ### Worker environment and authentication
package/dist/index.ts CHANGED
@@ -2711,23 +2711,25 @@ var taskParameters = Type.Object({
2711
2711
  subagent_type: Type.String({ description: "Subagent name" }),
2712
2712
  task_id: Type.Optional(Type.String({ description: "Task ID to resume" })),
2713
2713
  command: Type.Optional(Type.String({ description: "Originating command, if any" })),
2714
- background: Type.Optional(Type.Boolean({ description: "Return immediately and run in background" })),
2714
+ background: Type.Optional(Type.Boolean({
2715
+ description: "Return immediately and run in background; completion automatically triggers a follow-up turn"
2716
+ })),
2715
2717
  include_context: Type.Optional(Type.Boolean({
2716
2718
  description: "Seed the subagent prompt with a bounded summary of recent parent conversation"
2717
2719
  }))
2718
2720
  });
2721
+ var BACKGROUND_LAUNCH_GUIDANCE = [
2722
+ "Terminal notification: enabled.",
2723
+ "Automatic follow-up turn: enabled.",
2724
+ "Next action: do not poll or sleep to wait; continue only independent useful work, otherwise end this turn and wait for the completion notification."
2725
+ ].join(`
2726
+ `);
2719
2727
  var TasksToolSchema = Type.Object({
2720
- action: Type.Union([
2721
- Type.Literal("list"),
2722
- Type.Literal("inspect"),
2723
- Type.Literal("wait"),
2724
- Type.Literal("steer"),
2725
- Type.Literal("cancel")
2726
- ], {
2727
- description: 'Task management action: "list" to view task sessions, "inspect" for status/output, "wait" to await completion, "steer" to guide a running subagent, or "cancel" to stop an active task'
2728
+ action: Type.Union([Type.Literal("list"), Type.Literal("inspect"), Type.Literal("steer"), Type.Literal("cancel")], {
2729
+ description: 'Task management action: "list" to view task sessions, "inspect" for point-in-time status/output, "steer" to guide a running subagent, or "cancel" to stop an active task'
2728
2730
  }),
2729
2731
  task_id: Type.Optional(Type.String({
2730
- description: 'Task ID or unique prefix (required for "inspect", "wait", "steer", and "cancel")'
2732
+ description: 'Task ID or unique prefix (required for "inspect", "steer", and "cancel")'
2731
2733
  })),
2732
2734
  mode: Type.Optional(Type.Union([Type.Literal("all"), Type.Literal("background"), Type.Literal("foreground")], {
2733
2735
  description: 'Optional execution mode filter for "list": "all", "background", or "foreground"'
@@ -2745,17 +2747,12 @@ var TasksToolSchema = Type.Object({
2745
2747
  lines: Type.Optional(Type.Integer({
2746
2748
  minimum: 1,
2747
2749
  maximum: 500,
2748
- description: 'Number of transcript lines to return for "inspect" or "wait" (default 100)'
2750
+ description: 'Number of transcript lines to return for "inspect" (default 100)'
2749
2751
  })),
2750
2752
  offset: Type.Optional(Type.Integer({
2751
2753
  minimum: 0,
2752
2754
  description: '0-indexed starting line offset for "inspect" (omit for tail)'
2753
2755
  })),
2754
- timeout_ms: Type.Optional(Type.Integer({
2755
- minimum: 1,
2756
- maximum: 600000,
2757
- description: 'Optional timeout in milliseconds for "wait"'
2758
- })),
2759
2756
  message: Type.Optional(Type.String({
2760
2757
  description: 'Steering message to send to a running subagent (required for "steer")'
2761
2758
  }))
@@ -3537,6 +3534,10 @@ function registerSubagentWorker(pi, config) {
3537
3534
  name: "task",
3538
3535
  label: "Task",
3539
3536
  description: "Delegate a task to a Pi subagent process.",
3537
+ promptSnippet: "Delegate a task to a sandboxed Pi subagent process",
3538
+ promptGuidelines: [
3539
+ "After launching a task with background: true, continue only independent work that does not wait on the subagent; otherwise end the current turn and wait for the automatic completion follow-up turn. Do not poll tasks to wait."
3540
+ ],
3540
3541
  parameters: taskParameters,
3541
3542
  executionMode: "parallel",
3542
3543
  async execute(_id, input, signal, _onUpdate, ctx) {
@@ -3964,6 +3965,32 @@ ${this.primaryAgent.prompt}` };
3964
3965
  unsubscribe();
3965
3966
  done(undefined);
3966
3967
  };
3968
+ const runSaving = (errorPrefix, action) => {
3969
+ saving = true;
3970
+ action().catch((error) => ctx.ui.notify(`${errorPrefix}: ${formatError(error)}`, "error")).finally(() => {
3971
+ saving = false;
3972
+ tui.requestRender();
3973
+ });
3974
+ };
3975
+ const startSteering = (task) => {
3976
+ const input = new Input;
3977
+ input.focused = true;
3978
+ input.onEscape = () => {
3979
+ steering = undefined;
3980
+ tui.requestRender();
3981
+ };
3982
+ input.onSubmit = (value) => {
3983
+ const message = value.trim();
3984
+ if (!message) {
3985
+ ctx.ui.notify("Steering message must not be empty", "warning");
3986
+ return;
3987
+ }
3988
+ steering = undefined;
3989
+ this.steerTask(task, message).then(() => ctx.ui.notify(`Steered task ${task.id.slice(0, 8)}`, "info")).catch((error) => ctx.ui.notify(`Could not steer task: ${formatError(error)}`, "error")).finally(() => tui.requestRender());
3990
+ tui.requestRender();
3991
+ };
3992
+ steering = input;
3993
+ };
3967
3994
  const renderAgentsTab = (frame) => {
3968
3995
  const { tabs, pane, contentWidth, pad } = frame;
3969
3996
  const listedAgents = agentsForTab();
@@ -4118,8 +4145,7 @@ ${this.primaryAgent.prompt}` };
4118
4145
  ctx.ui.notify("Agent editing requires a trusted project", "warning");
4119
4146
  return;
4120
4147
  }
4121
- saving = true;
4122
- (async () => {
4148
+ runSaving("Could not edit agent", async () => {
4123
4149
  const document = prepareProjectAgentEditor(ctx.cwd, agent);
4124
4150
  const edited = await ctx.ui.editor(document.title, document.content);
4125
4151
  if (edited === undefined)
@@ -4127,9 +4153,6 @@ ${this.primaryAgent.prompt}` };
4127
4153
  await document.save(edited);
4128
4154
  await refreshAgentRuntime(agent.name);
4129
4155
  ctx.ui.notify(`Updated project agent ${agent.name}`, "info");
4130
- })().catch((error) => ctx.ui.notify(`Could not edit agent: ${formatError(error)}`, "error")).finally(() => {
4131
- saving = false;
4132
- tui.requestRender();
4133
4156
  });
4134
4157
  return;
4135
4158
  } else if (data === " " && agent && !saving) {
@@ -4138,13 +4161,10 @@ ${this.primaryAgent.prompt}` };
4138
4161
  return;
4139
4162
  }
4140
4163
  const disabled = !agent.disabled;
4141
- saving = true;
4142
- setAgentDisabledForScope(ctx.cwd, agent.name, disabled, "project").then(async () => {
4164
+ runSaving("Could not update agent", async () => {
4165
+ await setAgentDisabledForScope(ctx.cwd, agent.name, disabled, "project");
4143
4166
  await refreshAgentRuntime(agent.name);
4144
4167
  ctx.ui.notify(`${agent.name} ${disabled ? "disabled" : "enabled"} in project settings`, "info");
4145
- }).catch((error) => ctx.ui.notify(`Could not update agent: ${formatError(error)}`, "error")).finally(() => {
4146
- saving = false;
4147
- tui.requestRender();
4148
4168
  });
4149
4169
  return;
4150
4170
  } else if (data.toLowerCase() === "i" && agent && !saving) {
@@ -4154,13 +4174,10 @@ ${this.primaryAgent.prompt}` };
4154
4174
  }
4155
4175
  if (!disabledOverrides.project.has(agent.name))
4156
4176
  return;
4157
- saving = true;
4158
- clearAgentDisabledForScope(ctx.cwd, agent.name, "project").then(async () => {
4177
+ runSaving("Could not update agent", async () => {
4178
+ await clearAgentDisabledForScope(ctx.cwd, agent.name, "project");
4159
4179
  await refreshAgentRuntime(agent.name);
4160
4180
  ctx.ui.notify(`${agent.name} now inherits its global state`, "info");
4161
- }).catch((error) => ctx.ui.notify(`Could not update agent: ${formatError(error)}`, "error")).finally(() => {
4162
- saving = false;
4163
- tui.requestRender();
4164
4181
  });
4165
4182
  return;
4166
4183
  } else if (tab === "primary" && matchesKey(data, "return") && agent && !agent.disabled && isPrimaryAgent(agent)) {
@@ -4218,16 +4235,16 @@ ${this.primaryAgent.prompt}` };
4218
4235
  ...footer
4219
4236
  ]);
4220
4237
  };
4238
+ const item = (label, value) => ` ${theme.fg("dim", label.padEnd(20))} ${value}`;
4239
+ const shellReadLabel = (current) => current.windowsMode !== undefined ? "Policy (required)" : current.shellReadMode === "host" ? "Host-aligned" : "Policy";
4221
4240
  const renderTab = {
4222
4241
  overview: (frame) => renderSandboxTab(frame, () => {
4223
4242
  if (!overview)
4224
4243
  return [];
4225
4244
  const { contentWidth } = frame;
4226
4245
  const status = overview.noSandboxFlag ? { color: "warning", label: "Disabled by --no-sandbox" } : !overview.enabled ? { color: "warning", label: "Disabled by configuration" } : !overview.running ? { color: "warning", label: "Inactive" } : { color: "success", label: "Active" };
4227
- const shellReadMode = overview.windowsMode !== undefined ? "Policy (required)" : overview.shellReadMode === "host" ? "Host-aligned" : "Policy";
4228
4246
  const changePath = overview.changeScope === "project" ? overview.paths.project : overview.paths.global;
4229
4247
  const valueWidth = Math.max(10, contentWidth - 23);
4230
- const item = (label, value) => ` ${theme.fg("dim", label.padEnd(20))} ${value}`;
4231
4248
  const body = [
4232
4249
  `${theme.fg(status.color, "●")} ${theme.fg("text", status.label)}`,
4233
4250
  item("Configured", overview.enabled ? theme.fg("text", "Enabled") : theme.fg("warning", "Disabled")),
@@ -4236,7 +4253,7 @@ ${this.primaryAgent.prompt}` };
4236
4253
  "",
4237
4254
  theme.fg("accent", "Protection"),
4238
4255
  item("Network", overview.networkMode),
4239
- item("Shell reads", shellReadMode),
4256
+ item("Shell reads", shellReadLabel(overview)),
4240
4257
  item("Read rules", `${overview.config.filesystem.denyRead.length + overview.config.filesystem.denyReadAlways.length + overview.config.filesystem.allowRead.length} · ${overview.readRuleScope}`),
4241
4258
  item("Write rules", String(overview.config.filesystem.allowWrite.length + overview.config.filesystem.denyWrite.length + overview.config.filesystem.denyWriteAlways.length)),
4242
4259
  item("Session grants", String(overview.sessionDomains.length + overview.sessionReadPaths.length + overview.sessionWritePaths.length)),
@@ -4256,8 +4273,6 @@ ${this.primaryAgent.prompt}` };
4256
4273
  if (!overview)
4257
4274
  return [];
4258
4275
  const { listValue } = frame;
4259
- const shellReadMode = overview.windowsMode !== undefined ? "Policy (required)" : overview.shellReadMode === "host" ? "Host-aligned" : "Policy";
4260
- const item = (label, value) => ` ${theme.fg("dim", label.padEnd(20))} ${value}`;
4261
4276
  return [
4262
4277
  theme.fg("accent", "Network"),
4263
4278
  item("Mode", overview.networkMode),
@@ -4266,7 +4281,7 @@ ${this.primaryAgent.prompt}` };
4266
4281
  item("Unix sockets", overview.config.network.allowAllUnixSockets ? "all" : listValue(overview.config.network.allowUnixSockets)),
4267
4282
  "",
4268
4283
  theme.fg("accent", "Filesystem"),
4269
- item("Shell reads", shellReadMode),
4284
+ item("Shell reads", shellReadLabel(overview)),
4270
4285
  item("Read rules for", overview.readRuleScope),
4271
4286
  item("Denied reads", listValue(overview.config.filesystem.denyRead)),
4272
4287
  item("Always denied reads", listValue(overview.config.filesystem.denyReadAlways)),
@@ -4557,35 +4572,7 @@ ${this.primaryAgent.prompt}` };
4557
4572
  tab = "log";
4558
4573
  follow = true;
4559
4574
  scroll = 0;
4560
- const input = new Input;
4561
- input.focused = true;
4562
- input.onEscape = () => {
4563
- steering = undefined;
4564
- tui.requestRender();
4565
- };
4566
- input.onSubmit = (value) => {
4567
- const message = value.trim();
4568
- if (!message) {
4569
- ctx.ui.notify("Steering message must not be empty", "warning");
4570
- return;
4571
- }
4572
- steering = undefined;
4573
- (async () => {
4574
- const running = this.running.get(task.id);
4575
- if (running) {
4576
- await running.rpc.steer(message);
4577
- } else if ((task.state === "queued" || task.state === "running") && this.runPromises.has(task.id)) {
4578
- const pending = this.pendingSteers.get(task.id) ?? [];
4579
- pending.push(message);
4580
- this.pendingSteers.set(task.id, pending);
4581
- } else {
4582
- throw new Error("Task is no longer active");
4583
- }
4584
- ctx.ui.notify(`Steered task ${task.id.slice(0, 8)}`, "info");
4585
- })().catch((error) => ctx.ui.notify(`Could not steer task: ${formatError(error)}`, "error")).finally(() => tui.requestRender());
4586
- tui.requestRender();
4587
- };
4588
- steering = input;
4575
+ startSteering(task);
4589
4576
  } else if (data.toLowerCase() === "r") {
4590
4577
  const finished = tasks.filter((t) => t.state === "completed" || t.state === "error" || t.state === "cancelled" || t.state === "interrupted");
4591
4578
  if (finished.length === 0) {
@@ -4634,35 +4621,7 @@ ${this.primaryAgent.prompt}` };
4634
4621
  });
4635
4622
  return;
4636
4623
  } else if (matchesKey(data, "return") && (task.state === "queued" || task.state === "running") && task.kind !== "command") {
4637
- const input = new Input;
4638
- input.focused = true;
4639
- input.onEscape = () => {
4640
- steering = undefined;
4641
- tui.requestRender();
4642
- };
4643
- input.onSubmit = (value) => {
4644
- const message = value.trim();
4645
- if (!message) {
4646
- ctx.ui.notify("Steering message must not be empty", "warning");
4647
- return;
4648
- }
4649
- steering = undefined;
4650
- (async () => {
4651
- const running = this.running.get(task.id);
4652
- if (running) {
4653
- await running.rpc.steer(message);
4654
- } else if ((task.state === "queued" || task.state === "running") && this.runPromises.has(task.id)) {
4655
- const pending = this.pendingSteers.get(task.id) ?? [];
4656
- pending.push(message);
4657
- this.pendingSteers.set(task.id, pending);
4658
- } else {
4659
- throw new Error("Task is no longer active");
4660
- }
4661
- ctx.ui.notify(`Steered task ${task.id.slice(0, 8)}`, "info");
4662
- })().catch((error) => ctx.ui.notify(`Could not steer task: ${formatError(error)}`, "error")).finally(() => tui.requestRender());
4663
- tui.requestRender();
4664
- };
4665
- steering = input;
4624
+ startSteering(task);
4666
4625
  } else if (data.toLowerCase() === "f") {
4667
4626
  follow = !follow;
4668
4627
  } else if (matchesKey(data, "up")) {
@@ -4760,15 +4719,12 @@ ${this.primaryAgent.prompt}` };
4760
4719
  tui.requestRender();
4761
4720
  return;
4762
4721
  }
4763
- saving = true;
4764
- deleteProjectAgent(ctx.cwd, agent).then(async (deleted) => {
4722
+ runSaving("Could not delete agent", async () => {
4723
+ const deleted = await deleteProjectAgent(ctx.cwd, agent);
4765
4724
  if (!deleted)
4766
4725
  return;
4767
4726
  await refreshAgentRuntime(name);
4768
4727
  ctx.ui.notify(`Deleted project agent ${name}`, "info");
4769
- }).catch((error) => ctx.ui.notify(`Could not delete agent: ${formatError(error)}`, "error")).finally(() => {
4770
- saving = false;
4771
- tui.requestRender();
4772
4728
  });
4773
4729
  return;
4774
4730
  }
@@ -4863,7 +4819,12 @@ ${this.primaryAgent.prompt}` };
4863
4819
  return {
4864
4820
  name: "tasks",
4865
4821
  label: "Tasks",
4866
- description: "Inspect, list, and cancel background tasks and subagents managed by Landstrip.",
4822
+ description: "Inspect, list, steer, and cancel background tasks and subagents managed by Landstrip. Point-in-time management only, not a waiting primitive; background tasks automatically notify and trigger a follow-up turn when done.",
4823
+ promptSnippet: "Inspect, list, steer, or cancel background tasks and subagents without polling",
4824
+ promptGuidelines: [
4825
+ "Use tasks for point-in-time inspection, steering, or cancellation, not as a waiting primitive.",
4826
+ "Do not poll tasks with list or inspect while waiting for a background task to finish; completion automatically triggers a follow-up turn."
4827
+ ],
4867
4828
  parameters: TasksToolSchema,
4868
4829
  executionMode: "parallel",
4869
4830
  renderCall: (input, theme) => {
@@ -4871,7 +4832,7 @@ ${this.primaryAgent.prompt}` };
4871
4832
  const target = input.task_id ? ` ${input.task_id.slice(0, 8)}` : "";
4872
4833
  return new Text(`${theme.fg("toolTitle", theme.bold("Tasks"))} ${theme.fg("accent", action)}${theme.fg("dim", target)}`, 0, 0);
4873
4834
  },
4874
- execute: async (_toolCallId, input, signal, _onUpdate, callCtx) => {
4835
+ execute: async (_toolCallId, input, _signal, _onUpdate, callCtx) => {
4875
4836
  const ctx = boundCtx ?? callCtx;
4876
4837
  if (!ctx)
4877
4838
  throw new Error("Task context is unavailable");
@@ -4886,7 +4847,7 @@ ${this.primaryAgent.prompt}` };
4886
4847
  throw new Error(`Task ID prefix "${input.task_id}" matches ${matched.length} tasks`);
4887
4848
  return matched[0];
4888
4849
  };
4889
- const formatInspectResult = (task, timedOut = false) => {
4850
+ const formatInspectResult = (task) => {
4890
4851
  const details = taskDetails(task);
4891
4852
  const transcript = taskTranscript(task);
4892
4853
  const maxLines = Math.max(1, Math.min(500, input.lines ?? 100));
@@ -4894,18 +4855,18 @@ ${this.primaryAgent.prompt}` };
4894
4855
  const outputText = sliced.join(`
4895
4856
  `);
4896
4857
  const modeLabel = details.background ? "background" : "foreground";
4897
- const timeoutNote = timedOut ? `
4898
- Wait: timed out (task still active)` : "";
4858
+ const activeGuidance = details.background && (details.state === "queued" || details.state === "running") ? `
4859
+ Next action: task is still active; do not poll inspect in a loop—end this turn and wait for the automatic completion follow-up turn.` : "";
4899
4860
  const header = task.kind === "command" ? `$ bash ${task.command ?? task.description}
4900
4861
  Mode: ${modeLabel}
4901
4862
  CWD: ${task.cwd}
4902
4863
  State: ${details.state}
4903
4864
  Duration: ${taskDuration(details) ?? "unknown"}
4904
- Exit code: ${details.exitCode ?? "none"}${timeoutNote}` : `@${task.agent}: ${task.description}
4865
+ Exit code: ${details.exitCode ?? "none"}${activeGuidance}` : `@${task.agent}: ${task.description}
4905
4866
  Mode: ${modeLabel}
4906
4867
  State: ${details.state}
4907
4868
  Duration: ${taskDuration(details) ?? "unknown"}
4908
- Tool calls: ${task.toolCalls ?? 0}${timeoutNote}`;
4869
+ Tool calls: ${task.toolCalls ?? 0}${activeGuidance}`;
4909
4870
  return {
4910
4871
  content: [
4911
4872
  { type: "text", text: `${header}
@@ -4951,49 +4912,6 @@ ${outputText}` }
4951
4912
  const task = resolveSingleTask("inspect");
4952
4913
  return formatInspectResult(task);
4953
4914
  }
4954
- if (action === "wait") {
4955
- const task = resolveSingleTask("wait");
4956
- let timedOut = false;
4957
- const runPromise = this.runPromises.get(task.id);
4958
- if (runPromise && (task.state === "queued" || task.state === "running")) {
4959
- const waiters = [
4960
- runPromise.then(() => {
4961
- return;
4962
- }, () => {
4963
- return;
4964
- })
4965
- ];
4966
- let timer;
4967
- if (input.timeout_ms && input.timeout_ms > 0) {
4968
- waiters.push(new Promise((resolveWait) => {
4969
- timer = setTimeout(() => {
4970
- timedOut = true;
4971
- resolveWait();
4972
- }, input.timeout_ms);
4973
- }));
4974
- }
4975
- let onAbort;
4976
- if (signal) {
4977
- waiters.push(new Promise((_, rejectWait) => {
4978
- if (signal.aborted) {
4979
- rejectWait(new Error("Wait aborted"));
4980
- return;
4981
- }
4982
- onAbort = () => rejectWait(new Error("Wait aborted"));
4983
- signal.addEventListener("abort", onAbort, { once: true });
4984
- }));
4985
- }
4986
- try {
4987
- await Promise.race(waiters);
4988
- } finally {
4989
- if (timer !== undefined)
4990
- clearTimeout(timer);
4991
- if (onAbort && signal)
4992
- signal.removeEventListener("abort", onAbort);
4993
- }
4994
- }
4995
- return formatInspectResult(task, timedOut);
4996
- }
4997
4915
  if (action === "steer") {
4998
4916
  const task = resolveSingleTask("steer");
4999
4917
  const message = input.message?.trim();
@@ -5004,16 +4922,7 @@ ${outputText}` }
5004
4922
  if (task.state !== "running" && task.state !== "queued") {
5005
4923
  throw new Error(`Task ${task.id.slice(0, 8)} is no longer active (${task.state})`);
5006
4924
  }
5007
- const running = this.running.get(task.id);
5008
- if (running) {
5009
- await running.rpc.steer(message);
5010
- } else if (this.runPromises.has(task.id)) {
5011
- const pending = this.pendingSteers.get(task.id) ?? [];
5012
- pending.push(message);
5013
- this.pendingSteers.set(task.id, pending);
5014
- } else {
5015
- throw new Error(`Task ${task.id.slice(0, 8)} is no longer active`);
5016
- }
4925
+ await this.steerTask(task, message, `Task ${task.id.slice(0, 8)} is no longer active`);
5017
4926
  return {
5018
4927
  content: [
5019
4928
  {
@@ -5041,6 +4950,18 @@ ${outputText}` }
5041
4950
  }
5042
4951
  };
5043
4952
  }
4953
+ async steerTask(task, message, inactiveMessage = "Task is no longer active") {
4954
+ const running = this.running.get(task.id);
4955
+ if (running) {
4956
+ await running.rpc.steer(message);
4957
+ } else if ((task.state === "queued" || task.state === "running") && this.runPromises.has(task.id)) {
4958
+ const pending = this.pendingSteers.get(task.id) ?? [];
4959
+ pending.push(message);
4960
+ this.pendingSteers.set(task.id, pending);
4961
+ } else {
4962
+ throw new Error(inactiveMessage);
4963
+ }
4964
+ }
5044
4965
  async runBackgroundShellTask(params, ctx) {
5045
4966
  const taskId = randomUUID();
5046
4967
  const sessionId = this.activeSessionId ?? ctx.sessionManager.getSessionId();
@@ -5113,17 +5034,22 @@ ${outputText}` }
5113
5034
  text: `$ bash ${task.description} ${exitMsg} (${durationSec})`,
5114
5035
  finishedAt: Date.now()
5115
5036
  });
5116
- const commandOutput = task.outputFile && existsSync5(task.outputFile) ? readFileSync6(task.outputFile, "utf8").trim() : task.output ?? "";
5117
- const deliveryText = `Background command ${task.state === "completed" ? "completed" : "failed"}: ${task.description}
5037
+ if (!this.shuttingDown && !task.delivered && task.state !== "cancelled") {
5038
+ const rawOutput = task.outputFile && existsSync5(task.outputFile) ? readFileSync6(task.outputFile, "utf8").trim() : task.output ?? "";
5039
+ const commandOutput = rawOutput ? this.storeTaskText(task, rawOutput, "output") : "";
5040
+ if (commandOutput)
5041
+ task.output = commandOutput;
5042
+ const deliveryText = `Background command ${task.state === "completed" ? "completed" : "failed"}: ${task.description}
5118
5043
 
5119
5044
  ${renderTaskResult(task.id, task.state, commandOutput || (task.error ?? ""))}`;
5120
- const delivered = await this.deliverBackground(task, deliveryText);
5121
- if (delivered) {
5122
- task.delivered = true;
5123
- this.persist(task);
5124
- }
5125
- if (task.state === "error") {
5126
- ctx.ui.notify(`Background command failed: ${task.description}`, "error");
5045
+ const delivered = await this.deliverBackground(task, deliveryText);
5046
+ if (delivered) {
5047
+ task.delivered = true;
5048
+ this.persist(task);
5049
+ }
5050
+ if (task.state === "error") {
5051
+ ctx.ui.notify(`Background command failed: ${task.description}`, "error");
5052
+ }
5127
5053
  }
5128
5054
  this.notifyTaskUpdate();
5129
5055
  this.updateTaskWidget(ctx);
@@ -5135,10 +5061,14 @@ ${renderTaskResult(task.id, task.state, commandOutput || (task.error ?? ""))}`;
5135
5061
  content: [
5136
5062
  {
5137
5063
  type: "text",
5138
- text: `Command launched in background.
5139
- Task ID: ${taskId}
5140
- Output file: ${outputFile}
5141
- Use /landstrip-subagents tasks to view live progress or cancel.`
5064
+ text: [
5065
+ "Command launched in background.",
5066
+ `Task ID: ${taskId}`,
5067
+ `Output file: ${outputFile}`,
5068
+ BACKGROUND_LAUNCH_GUIDANCE,
5069
+ "Use /landstrip-subagents tasks to view live progress or cancel."
5070
+ ].join(`
5071
+ `)
5142
5072
  }
5143
5073
  ],
5144
5074
  details: {
@@ -5162,6 +5092,10 @@ Use /landstrip-subagents tasks to view live progress or cancel.`
5162
5092
 
5163
5093
  Available subagents:
5164
5094
  ${descriptions}` : ""),
5095
+ promptSnippet: "Delegate a task to a sandboxed Pi subagent process",
5096
+ promptGuidelines: [
5097
+ "After launching a task with background: true, continue only independent work that does not wait on the subagent; otherwise end the current turn and wait for the automatic completion follow-up turn. Do not poll tasks to wait."
5098
+ ],
5165
5099
  parameters: taskParameters,
5166
5100
  executionMode: "parallel",
5167
5101
  renderCall: (input, theme) => {
@@ -5326,7 +5260,8 @@ ${catalog.diagnostics.join(`
5326
5260
  this.notifyWhenDone(task, existingRun, ctx);
5327
5261
  }
5328
5262
  const state = task.state === "queued" ? "queued" : "running";
5329
- const status = state === "queued" ? "Background task queued" : "Background task updated";
5263
+ const status = `${state === "queued" ? "Background task queued" : "Background task updated"}
5264
+ ${BACKGROUND_LAUNCH_GUIDANCE}`;
5330
5265
  return { task, text: renderTaskResult(task.id, state, status), state };
5331
5266
  }
5332
5267
  this.claimForeground(task.id);
@@ -5379,7 +5314,8 @@ ${catalog.diagnostics.join(`
5379
5314
  this.notifyWhenDone(task, run, ctx);
5380
5315
  return {
5381
5316
  task,
5382
- text: renderTaskResult(task.id, "queued", "Background task queued"),
5317
+ text: renderTaskResult(task.id, "queued", `Background task queued
5318
+ ${BACKGROUND_LAUNCH_GUIDANCE}`),
5383
5319
  state: "queued"
5384
5320
  };
5385
5321
  }
@@ -6351,7 +6287,7 @@ var LandstripBashSchema = Type2.Object({
6351
6287
  command: Type2.String({ description: "Shell command to execute" }),
6352
6288
  timeout: Type2.Optional(Type2.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
6353
6289
  background: Type2.Optional(Type2.Boolean({
6354
- description: "Run command asynchronously in the background and return a task ID"
6290
+ description: "Run command asynchronously in the background and return a task ID immediately; completion automatically triggers a follow-up turn"
6355
6291
  })),
6356
6292
  name: Type2.Optional(Type2.String({
6357
6293
  description: "Concise label for the background command execution"
@@ -8042,6 +7978,10 @@ ${message}
8042
7978
  ...localBash,
8043
7979
  label: "bash (landstrip)",
8044
7980
  description: "Execute commands with the active Landstrip shell provider.",
7981
+ promptGuidelines: [
7982
+ ...localBash.promptGuidelines ?? [],
7983
+ "After launching a bash command with background: true, continue only independent work that does not wait on the command; otherwise end the current turn and wait for the automatic completion follow-up turn. Do not sleep or poll tasks to wait."
7984
+ ],
8045
7985
  parameters: LandstripBashSchema,
8046
7986
  renderCall(args, theme, context) {
8047
7987
  const bashArgs = args;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.20.3",
3
+ "version": "0.20.5",
4
4
  "description": "Sandboxed Bash and process-backed agents for Pi",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -39,7 +39,7 @@
39
39
  "ci:test": "npm run test"
40
40
  },
41
41
  "dependencies": {
42
- "@landstrip/landstrip-api": "^0.20.3",
42
+ "@landstrip/landstrip-api": "^0.20.5",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "undici": "^8.9.0",