From 8b19cfaff6bcf50c8b641a07286ba84528295f1a Mon Sep 17 00:00:00 2001
From: Personal Stack Agent
Date: Thu, 4 Jun 2026 08:38:01 +0000
Subject: [PATCH 1/3] agents: stage large terminal inputs
---
.../agentgateway/config/GatewayProperties.kt | 6 ++
.../agentgateway/tmux/AgentSessionManager.kt | 48 ++++++++++
.../agentgateway/tmux/StagedInput.kt | 7 ++
.../agentgateway/web/AgentController.kt | 13 +++
.../agentgateway/web/ErrorAdvice.kt | 4 +
.../agentgateway/web/dto/Dtos.kt | 11 +++
.../src/main/resources/application.yml | 3 +
.../tmux/AgentSessionManagerTest.kt | 47 +++++++++-
.../agentgateway/web/AgentControllerTest.kt | 33 +++++++
services/assistant-api/openapi.json | 73 +++++++++++++++
.../domain/port/AgentGatewayClient.kt | 13 +++
.../integration/HttpAgentGatewayClient.kt | 28 ++++++
.../web/AgentSessionController.kt | 27 ++++++
.../infrastructure/web/dto/WorkspaceDtos.kt | 11 +++
.../web/AgentSessionControllerTest.kt | 92 +++++++++++++++++++
services/assistant-ui/src/api/generated.ts | 53 +++++++++++
.../__tests__/WorkspaceView.test.ts | 45 ++++++++-
.../workspaces/services/workspaceService.ts | 14 ++-
.../src/features/workspaces/types/index.ts | 6 ++
.../workspaces/views/WorkspaceView.vue | 86 +++++++++++++++++
20 files changed, 617 insertions(+), 3 deletions(-)
create mode 100644 services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/tmux/StagedInput.kt
create mode 100644 services/assistant-api/src/test/kotlin/com/jorisjonkers/personalstack/assistant/infrastructure/web/AgentSessionControllerTest.kt
diff --git a/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/config/GatewayProperties.kt b/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/config/GatewayProperties.kt
index 541f810b..cf6f0be1 100644
--- a/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/config/GatewayProperties.kt
+++ b/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/config/GatewayProperties.kt
@@ -8,6 +8,7 @@ data class GatewayProperties(
val tmux: Tmux,
val cli: Cli,
val git: Git,
+ val stagedInputs: StagedInputs = StagedInputs(),
) {
data class Tmux(
val socketName: String,
@@ -36,4 +37,9 @@ data class GatewayProperties(
data class Git(
val deployKeyDir: String,
)
+
+ data class StagedInputs(
+ val dirName: String = ".agent-inputs",
+ val maxBytes: Long = 5L * 1024 * 1024,
+ )
}
diff --git a/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/tmux/AgentSessionManager.kt b/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/tmux/AgentSessionManager.kt
index c3be986e..15ec6e91 100644
--- a/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/tmux/AgentSessionManager.kt
+++ b/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/tmux/AgentSessionManager.kt
@@ -9,6 +9,8 @@ import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.time.Instant
+import java.time.ZoneOffset
+import java.time.format.DateTimeFormatter
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
@@ -116,6 +118,33 @@ class AgentSessionManager(
tmux.sendKeys(session.tmuxSession, input, enter = enter)
}
+ fun stageInput(
+ id: String,
+ content: String,
+ requestedName: String?,
+ ): StagedInput {
+ val session = sessions[id] ?: error("unknown agent: $id")
+ val bytes = content.toByteArray(Charsets.UTF_8)
+ require(bytes.isNotEmpty()) { "staged input content is empty" }
+ require(bytes.size.toLong() <= props.stagedInputs.maxBytes) {
+ "staged input exceeds ${props.stagedInputs.maxBytes} bytes"
+ }
+
+ val root = Path(session.cwd).toAbsolutePath().normalize()
+ val dir = root.resolve(props.stagedInputs.dirName).normalize()
+ require(dir.startsWith(root)) { "staged input directory must stay inside the workspace" }
+
+ Files.createDirectories(dir)
+ val safeName = safeFileName(requestedName)
+ val fileName = "${timestamp()}-${UUID.randomUUID().toString().take(ID_PREVIEW_CHARS)}-$safeName"
+ val target = dir.resolve(fileName).normalize()
+ require(target.startsWith(dir)) { "staged input path must stay inside the staging directory" }
+
+ Files.write(target, bytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)
+ log.info("staged {} bytes for agent {} at {}", bytes.size, id, target)
+ return StagedInput(path = target.toString(), bytes = bytes.size.toLong(), name = safeName)
+ }
+
fun capture(
id: String,
historyLines: Int = 1_000,
@@ -190,8 +219,27 @@ class AgentSessionManager(
AgentKind.SHELL -> listOf("/bin/bash", "-l") to null
}
+ private fun safeFileName(requestedName: String?): String {
+ val raw = requestedName?.trim()?.takeIf { it.isNotBlank() } ?: DEFAULT_STAGED_INPUT_NAME
+ val leaf = raw.replace('\\', '/').substringAfterLast('/')
+ val safe =
+ SAFE_NAME_CHARS
+ .replace(leaf, "-")
+ .trim('.', '-', '_')
+ .take(MAX_STAGED_INPUT_NAME_CHARS)
+ return safe.takeIf { it.isNotBlank() } ?: DEFAULT_STAGED_INPUT_NAME
+ }
+
+ private fun timestamp(): String = STAGED_INPUT_TIMESTAMP.format(Instant.now())
+
companion object {
/** Sentinel value for [resumeCliSessionId] that tells the gateway to run `codex resume --last`. */
const val CODEX_RESUME_LAST = "LATEST"
+ private const val DEFAULT_STAGED_INPUT_NAME = "input.txt"
+ private const val ID_PREVIEW_CHARS = 8
+ private const val MAX_STAGED_INPUT_NAME_CHARS = 80
+ private val SAFE_NAME_CHARS = Regex("[^A-Za-z0-9._-]+")
+ private val STAGED_INPUT_TIMESTAMP =
+ DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss-SSS").withZone(ZoneOffset.UTC)
}
}
diff --git a/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/tmux/StagedInput.kt b/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/tmux/StagedInput.kt
new file mode 100644
index 00000000..920101d3
--- /dev/null
+++ b/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/tmux/StagedInput.kt
@@ -0,0 +1,7 @@
+package com.jorisjonkers.personalstack.agentgateway.tmux
+
+data class StagedInput(
+ val path: String,
+ val bytes: Long,
+ val name: String,
+)
diff --git a/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/web/AgentController.kt b/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/web/AgentController.kt
index 10fa16e5..843d55b7 100644
--- a/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/web/AgentController.kt
+++ b/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/web/AgentController.kt
@@ -5,6 +5,8 @@ import com.jorisjonkers.personalstack.agentgateway.tmux.AgentSessionManager
import com.jorisjonkers.personalstack.agentgateway.web.dto.AgentResponse
import com.jorisjonkers.personalstack.agentgateway.web.dto.SendInputRequest
import com.jorisjonkers.personalstack.agentgateway.web.dto.SpawnAgentRequest
+import com.jorisjonkers.personalstack.agentgateway.web.dto.StageInputRequest
+import com.jorisjonkers.personalstack.agentgateway.web.dto.StagedInputResponse
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
@@ -47,6 +49,17 @@ class AgentController(
return ResponseEntity.accepted().build()
}
+ @PostMapping("/{id}/staged-inputs")
+ fun stageInput(
+ @PathVariable id: String,
+ @RequestBody req: StageInputRequest,
+ ): ResponseEntity {
+ val staged = sessions.stageInput(id, req.content, req.name)
+ return ResponseEntity
+ .status(HttpStatus.CREATED)
+ .body(StagedInputResponse(path = staged.path, bytes = staged.bytes, name = staged.name))
+ }
+
@GetMapping("/{id}/capture")
fun capture(
@PathVariable id: String,
diff --git a/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/web/ErrorAdvice.kt b/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/web/ErrorAdvice.kt
index a3156f30..a96d4358 100644
--- a/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/web/ErrorAdvice.kt
+++ b/services/agent-gateway/src/main/kotlin/com/jorisjonkers/personalstack/agentgateway/web/ErrorAdvice.kt
@@ -30,6 +30,10 @@ class ErrorAdvice {
fun onIllegalState(e: IllegalStateException): ResponseEntity
+
+
+
+
+
From 707fc44e323b0448e90e0d974f1722578451b7d3 Mon Sep 17 00:00:00 2001
From: Personal Stack Agent
Date: Thu, 4 Jun 2026 08:48:01 +0000
Subject: [PATCH 2/3] agents: format skill metadata
---
.../skills/add-public-service/agents/openai.yaml | 6 +++---
.../agent-session-bootstrap/agents/openai.yaml | 16 ++++++++--------
.agents/skills/fleet-change/agents/openai.yaml | 6 +++---
.agents/skills/kb-first/agents/openai.yaml | 16 ++++++++--------
.agents/skills/open-pr/agents/openai.yaml | 6 +++---
.agents/skills/run-tests/agents/openai.yaml | 6 +++---
.agents/skills/token-economy/agents/openai.yaml | 16 ++++++++--------
7 files changed, 36 insertions(+), 36 deletions(-)
diff --git a/.agents/skills/add-public-service/agents/openai.yaml b/.agents/skills/add-public-service/agents/openai.yaml
index 9745ec16..8084bcff 100644
--- a/.agents/skills/add-public-service/agents/openai.yaml
+++ b/.agents/skills/add-public-service/agents/openai.yaml
@@ -1,4 +1,4 @@
interface:
- display_name: "Add Public Service"
- short_description: "Add stack public services safely"
- default_prompt: "Use $add-public-service to add a new SSO-protected public app."
+ display_name: 'Add Public Service'
+ short_description: 'Add stack public services safely'
+ default_prompt: 'Use $add-public-service to add a new SSO-protected public app.'
diff --git a/.agents/skills/agent-session-bootstrap/agents/openai.yaml b/.agents/skills/agent-session-bootstrap/agents/openai.yaml
index 15d36838..23a7f072 100644
--- a/.agents/skills/agent-session-bootstrap/agents/openai.yaml
+++ b/.agents/skills/agent-session-bootstrap/agents/openai.yaml
@@ -1,11 +1,11 @@
interface:
- display_name: "Agent Session Bootstrap"
- short_description: "Bootstrap durable agent context"
- default_prompt: "Use $agent-session-bootstrap to set up agent hooks, skills, and durable instructions."
+ display_name: 'Agent Session Bootstrap'
+ short_description: 'Bootstrap durable agent context'
+ default_prompt: 'Use $agent-session-bootstrap to set up agent hooks, skills, and durable instructions.'
dependencies:
tools:
- - type: "mcp"
- value: "knowledge"
- description: "Knowledge-base MCP server"
- transport: "streamable_http"
- url: "http://knowledge-api.knowledge-system.svc.cluster.local:8080/mcp"
+ - type: 'mcp'
+ value: 'knowledge'
+ description: 'Knowledge-base MCP server'
+ transport: 'streamable_http'
+ url: 'http://knowledge-api.knowledge-system.svc.cluster.local:8080/mcp'
diff --git a/.agents/skills/fleet-change/agents/openai.yaml b/.agents/skills/fleet-change/agents/openai.yaml
index 4fab0fad..1ce188f2 100644
--- a/.agents/skills/fleet-change/agents/openai.yaml
+++ b/.agents/skills/fleet-change/agents/openai.yaml
@@ -1,4 +1,4 @@
interface:
- display_name: "Fleet Change"
- short_description: "Render and validate fleet edits"
- default_prompt: "Use $fleet-change to make this fleet.yaml update safely."
+ display_name: 'Fleet Change'
+ short_description: 'Render and validate fleet edits'
+ default_prompt: 'Use $fleet-change to make this fleet.yaml update safely.'
diff --git a/.agents/skills/kb-first/agents/openai.yaml b/.agents/skills/kb-first/agents/openai.yaml
index 0b161efe..95344b38 100644
--- a/.agents/skills/kb-first/agents/openai.yaml
+++ b/.agents/skills/kb-first/agents/openai.yaml
@@ -1,11 +1,11 @@
interface:
- display_name: "KB First"
- short_description: "Recall and capture KB context"
- default_prompt: "Use $kb-first to gather compact KB context before this task."
+ display_name: 'KB First'
+ short_description: 'Recall and capture KB context'
+ default_prompt: 'Use $kb-first to gather compact KB context before this task.'
dependencies:
tools:
- - type: "mcp"
- value: "knowledge"
- description: "Knowledge-base MCP server"
- transport: "streamable_http"
- url: "http://knowledge-api.knowledge-system.svc.cluster.local:8080/mcp"
+ - type: 'mcp'
+ value: 'knowledge'
+ description: 'Knowledge-base MCP server'
+ transport: 'streamable_http'
+ url: 'http://knowledge-api.knowledge-system.svc.cluster.local:8080/mcp'
diff --git a/.agents/skills/open-pr/agents/openai.yaml b/.agents/skills/open-pr/agents/openai.yaml
index 58ecf2de..2d2fdba5 100644
--- a/.agents/skills/open-pr/agents/openai.yaml
+++ b/.agents/skills/open-pr/agents/openai.yaml
@@ -1,4 +1,4 @@
interface:
- display_name: "Open PR"
- short_description: "Create repo-standard pull requests"
- default_prompt: "Use $open-pr to open a personal-stack pull request."
+ display_name: 'Open PR'
+ short_description: 'Create repo-standard pull requests'
+ default_prompt: 'Use $open-pr to open a personal-stack pull request.'
diff --git a/.agents/skills/run-tests/agents/openai.yaml b/.agents/skills/run-tests/agents/openai.yaml
index 0decebe1..22d6d7b1 100644
--- a/.agents/skills/run-tests/agents/openai.yaml
+++ b/.agents/skills/run-tests/agents/openai.yaml
@@ -1,4 +1,4 @@
interface:
- display_name: "Run Tests"
- short_description: "Choose monorepo validation commands"
- default_prompt: "Use $run-tests to validate the files touched by this change."
+ display_name: 'Run Tests'
+ short_description: 'Choose monorepo validation commands'
+ default_prompt: 'Use $run-tests to validate the files touched by this change.'
diff --git a/.agents/skills/token-economy/agents/openai.yaml b/.agents/skills/token-economy/agents/openai.yaml
index 16e8c0f7..d2e98a71 100644
--- a/.agents/skills/token-economy/agents/openai.yaml
+++ b/.agents/skills/token-economy/agents/openai.yaml
@@ -1,11 +1,11 @@
interface:
- display_name: "Token Economy"
- short_description: "Reduce agent token usage cleanly"
- default_prompt: "Use $token-economy to keep this agent workflow token-efficient."
+ display_name: 'Token Economy'
+ short_description: 'Reduce agent token usage cleanly'
+ default_prompt: 'Use $token-economy to keep this agent workflow token-efficient.'
dependencies:
tools:
- - type: "mcp"
- value: "knowledge"
- description: "Knowledge-base MCP server"
- transport: "streamable_http"
- url: "http://knowledge-api.knowledge-system.svc.cluster.local:8080/mcp"
+ - type: 'mcp'
+ value: 'knowledge'
+ description: 'Knowledge-base MCP server'
+ transport: 'streamable_http'
+ url: 'http://knowledge-api.knowledge-system.svc.cluster.local:8080/mcp'
From 0c3ba4c6963b84f86780c9272df3f45a998faed6 Mon Sep 17 00:00:00 2001
From: Personal Stack Agent
Date: Thu, 4 Jun 2026 08:53:02 +0000
Subject: [PATCH 3/3] assistant: align staged input contract
---
services/assistant-api/openapi.json | 54 +++++++++++-----------
services/assistant-ui/src/api/generated.ts | 32 ++++++-------
2 files changed, 43 insertions(+), 43 deletions(-)
diff --git a/services/assistant-api/openapi.json b/services/assistant-api/openapi.json
index 224df14e..17da61e5 100644
--- a/services/assistant-api/openapi.json
+++ b/services/assistant-api/openapi.json
@@ -104,10 +104,10 @@
}
}
},
- "/api/v1/workspaces/{workspaceId}/sessions/{sessionId}/input" : {
+ "/api/v1/workspaces/{workspaceId}/sessions/{sessionId}/staged-inputs" : {
"post" : {
"tags" : [ "agent-session-controller" ],
- "operationId" : "send",
+ "operationId" : "stageInput",
"parameters" : [ {
"name" : "workspaceId",
"in" : "path",
@@ -129,7 +129,7 @@
"content" : {
"application/json" : {
"schema" : {
- "$ref" : "#/components/schemas/SendUserInputRequest"
+ "$ref" : "#/components/schemas/StageInputRequest"
}
}
},
@@ -137,15 +137,22 @@
},
"responses" : {
"200" : {
- "description" : "OK"
+ "description" : "OK",
+ "content" : {
+ "*/*" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/StagedInputResponse"
+ }
+ }
+ }
}
}
}
},
- "/api/v1/workspaces/{workspaceId}/sessions/{sessionId}/staged-inputs" : {
+ "/api/v1/workspaces/{workspaceId}/sessions/{sessionId}/input" : {
"post" : {
"tags" : [ "agent-session-controller" ],
- "operationId" : "stageInput",
+ "operationId" : "send",
"parameters" : [ {
"name" : "workspaceId",
"in" : "path",
@@ -167,7 +174,7 @@
"content" : {
"application/json" : {
"schema" : {
- "$ref" : "#/components/schemas/StageInputRequest"
+ "$ref" : "#/components/schemas/SendUserInputRequest"
}
}
},
@@ -175,14 +182,7 @@
},
"responses" : {
"200" : {
- "description" : "OK",
- "content" : {
- "*/*" : {
- "schema" : {
- "$ref" : "#/components/schemas/StagedInputResponse"
- }
- }
- }
+ "description" : "OK"
}
}
}
@@ -1226,18 +1226,6 @@
},
"required" : [ "kind" ]
},
- "SendUserInputRequest" : {
- "type" : "object",
- "properties" : {
- "text" : {
- "type" : "string"
- },
- "enter" : {
- "type" : "boolean"
- }
- },
- "required" : [ "enter", "text" ]
- },
"StageInputRequest" : {
"type" : "object",
"properties" : {
@@ -1266,6 +1254,18 @@
},
"required" : [ "bytes", "name", "path" ]
},
+ "SendUserInputRequest" : {
+ "type" : "object",
+ "properties" : {
+ "text" : {
+ "type" : "string"
+ },
+ "enter" : {
+ "type" : "boolean"
+ }
+ },
+ "required" : [ "enter", "text" ]
+ },
"OpenPrRequest" : {
"type" : "object",
"properties" : {
diff --git a/services/assistant-ui/src/api/generated.ts b/services/assistant-ui/src/api/generated.ts
index 5c37381b..49cb0def 100644
--- a/services/assistant-ui/src/api/generated.ts
+++ b/services/assistant-ui/src/api/generated.ts
@@ -44,7 +44,7 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/workspaces/{workspaceId}/sessions/{sessionId}/input": {
+ "/api/v1/workspaces/{workspaceId}/sessions/{sessionId}/staged-inputs": {
parameters: {
query?: never;
header?: never;
@@ -53,14 +53,14 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations["send"];
+ post: operations["stageInput"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/workspaces/{workspaceId}/sessions/{sessionId}/staged-inputs": {
+ "/api/v1/workspaces/{workspaceId}/sessions/{sessionId}/input": {
parameters: {
query?: never;
header?: never;
@@ -69,7 +69,7 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations["stageInput"];
+ post: operations["send"];
delete?: never;
options?: never;
head?: never;
@@ -491,10 +491,6 @@ export interface components {
/** @enum {string} */
kind: "CLAUDE" | "CODEX" | "SHELL";
};
- SendUserInputRequest: {
- text: string;
- enter: boolean;
- };
StageInputRequest: {
content: string;
name?: string | null;
@@ -505,6 +501,10 @@ export interface components {
bytes: number;
name: string;
};
+ SendUserInputRequest: {
+ text: string;
+ enter: boolean;
+ };
OpenPrRequest: {
repoDir: string;
title: string;
@@ -744,7 +744,7 @@ export interface operations {
};
};
};
- send: {
+ stageInput: {
parameters: {
query?: never;
header?: never;
@@ -756,7 +756,7 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["SendUserInputRequest"];
+ "application/json": components["schemas"]["StageInputRequest"];
};
};
responses: {
@@ -765,11 +765,13 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "*/*": components["schemas"]["StagedInputResponse"];
+ };
};
};
};
- stageInput: {
+ send: {
parameters: {
query?: never;
header?: never;
@@ -781,7 +783,7 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["StageInputRequest"];
+ "application/json": components["schemas"]["SendUserInputRequest"];
};
};
responses: {
@@ -790,9 +792,7 @@ export interface operations {
headers: {
[name: string]: unknown;
};
- content: {
- "*/*": components["schemas"]["StagedInputResponse"];
- };
+ content?: never;
};
};
};