@pstdio/pocketcoder-cli 0.2.0 → 0.3.0

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 (2) hide show
  1. package/dist/index.js +1638 -155
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -366,7 +366,7 @@ var require_main = __commonJS((exports, module) => {
366
366
  });
367
367
 
368
368
  // src/index.ts
369
- import { randomUUID as randomUUID12 } from "crypto";
369
+ import { randomUUID as randomUUID14 } from "crypto";
370
370
  import { existsSync as existsSync2, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
371
371
  import { dirname as dirname7, join as join6, resolve as resolve8 } from "path";
372
372
 
@@ -453,80 +453,6 @@ function sha256Hex(input) {
453
453
  function digestOf(value) {
454
454
  return `sha256:${sha256Hex(canonicalJson(value))}`;
455
455
  }
456
- // ../contracts/src/duration.ts
457
- var DURATION_RE = /^(\d+)(ms|s|m|h)$/;
458
- var UNIT_MS = {
459
- ms: 1,
460
- s: 1000,
461
- m: 60000,
462
- h: 3600000
463
- };
464
- function isDuration(value) {
465
- return DURATION_RE.test(value);
466
- }
467
- function parseDurationMs(value) {
468
- const match = DURATION_RE.exec(value);
469
- if (!match) {
470
- throw new Error(`invalid duration: ${value}`);
471
- }
472
- const amount = Number(match[1]);
473
- const unit = UNIT_MS[match[2]];
474
- if (unit === undefined) {
475
- throw new Error(`invalid duration unit: ${value}`);
476
- }
477
- return amount * unit;
478
- }
479
- // ../contracts/src/errors.ts
480
- var ERROR_CODES = {
481
- "auth.invalid_key": 401,
482
- "auth.missing_scope": 403,
483
- "auth.disabled_principal": 403,
484
- "validation.invalid": 400,
485
- "idempotency.conflict": 409,
486
- "capacity.queue_full": 429,
487
- "capacity.waiters_full": 429,
488
- "template.not_found": 404,
489
- "template.version_not_found": 404,
490
- "template.not_authorized": 403,
491
- "workspace.not_found": 404,
492
- "workspace.not_ready": 409,
493
- "workspace.terminal": 410,
494
- "workspace.disconnected": 503,
495
- "workspace.persistence_not_enabled": 409,
496
- "workspace.preserving": 409,
497
- "checkpoint.not_found": 404,
498
- "checkpoint.not_ready": 409,
499
- "checkpoint.none_ready": 409,
500
- "checkpoint.corrupt": 409,
501
- "checkpoint.quota_exceeded": 413,
502
- "checkpoint.in_use": 409,
503
- "restore.template_not_authorized": 403,
504
- "restore.image_unavailable": 409,
505
- "restore.incompatible": 409,
506
- "operation.conflict": 409,
507
- "source.not_allowed": 422,
508
- "source.invalid_revision": 422,
509
- "secret.unavailable": 503,
510
- "storage.capacity_exhausted": 507,
511
- "relay.body_too_large": 413,
512
- "relay.route_not_allowed": 422,
513
- "relay.deadline_exceeded": 504,
514
- "relay.upstream_error": 502,
515
- "internal.error": 500
516
- };
517
-
518
- class ApiError extends Error {
519
- code;
520
- status;
521
- constructor(code, message) {
522
- super(message);
523
- this.code = code;
524
- this.status = ERROR_CODES[code];
525
- }
526
- }
527
- function errorEnvelope(code, message, requestId) {
528
- return { error: { code, message, request_id: requestId } };
529
- }
530
456
  // ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
531
457
  var exports_external = {};
532
458
  __export(exports_external, {
@@ -14803,6 +14729,108 @@ function date4(params) {
14803
14729
 
14804
14730
  // ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
14805
14731
  config(en_default());
14732
+ // ../contracts/src/conversation.ts
14733
+ var CONVERSATION_ROLES = ["user", "assistant", "system", "tool"];
14734
+ var ConversationMetadataSchema = exports_external.record(exports_external.string().min(1).max(64), exports_external.string().max(512)).refine((value) => Object.keys(value).length <= 32, "conversation metadata has at most 32 keys");
14735
+ var ConversationMessageInputSchema = exports_external.object({
14736
+ message_id: exports_external.string().min(1).max(256),
14737
+ role: exports_external.enum(CONVERSATION_ROLES),
14738
+ content: exports_external.string().max(256 * 1024),
14739
+ occurred_at: exports_external.iso.datetime(),
14740
+ metadata: ConversationMetadataSchema.default({})
14741
+ });
14742
+ var ConversationMessageResourceSchema = ConversationMessageInputSchema.extend({
14743
+ seq: exports_external.number().int().positive()
14744
+ });
14745
+ var ConversationListQuerySchema = exports_external.object({
14746
+ after: exports_external.coerce.number().int().nonnegative().default(0),
14747
+ limit: exports_external.coerce.number().int().positive().max(200).default(100)
14748
+ });
14749
+ var ConversationResumeOutcomeSchema = exports_external.object({
14750
+ status: exports_external.literal("supported"),
14751
+ reason: exports_external.null(),
14752
+ source_workspace_id: exports_external.uuid(),
14753
+ checkpoint_id: exports_external.uuid()
14754
+ });
14755
+ // ../contracts/src/duration.ts
14756
+ var DURATION_RE = /^(\d+)(ms|s|m|h)$/;
14757
+ var UNIT_MS = {
14758
+ ms: 1,
14759
+ s: 1000,
14760
+ m: 60000,
14761
+ h: 3600000
14762
+ };
14763
+ function isDuration(value) {
14764
+ return DURATION_RE.test(value);
14765
+ }
14766
+ function parseDurationMs(value) {
14767
+ const match = DURATION_RE.exec(value);
14768
+ if (!match) {
14769
+ throw new Error(`invalid duration: ${value}`);
14770
+ }
14771
+ const amount = Number(match[1]);
14772
+ const unit = UNIT_MS[match[2]];
14773
+ if (unit === undefined) {
14774
+ throw new Error(`invalid duration unit: ${value}`);
14775
+ }
14776
+ return amount * unit;
14777
+ }
14778
+ // ../contracts/src/errors.ts
14779
+ var ERROR_CODES = {
14780
+ "auth.invalid_key": 401,
14781
+ "auth.missing_scope": 403,
14782
+ "auth.disabled_principal": 403,
14783
+ "validation.invalid": 400,
14784
+ "idempotency.conflict": 409,
14785
+ "capacity.queue_full": 429,
14786
+ "capacity.waiters_full": 429,
14787
+ "template.not_found": 404,
14788
+ "template.version_not_found": 404,
14789
+ "template.not_authorized": 403,
14790
+ "workspace.not_found": 404,
14791
+ "workspace.not_ready": 409,
14792
+ "workspace.terminal": 410,
14793
+ "workspace.disconnected": 503,
14794
+ "workspace.persistence_not_enabled": 409,
14795
+ "workspace.preserving": 409,
14796
+ "checkpoint.not_found": 404,
14797
+ "checkpoint.not_ready": 409,
14798
+ "checkpoint.none_ready": 409,
14799
+ "checkpoint.corrupt": 409,
14800
+ "checkpoint.quota_exceeded": 413,
14801
+ "checkpoint.in_use": 409,
14802
+ "restore.template_not_authorized": 403,
14803
+ "restore.image_unavailable": 409,
14804
+ "restore.incompatible": 409,
14805
+ "resume.unsupported": 409,
14806
+ "conversation.expired": 410,
14807
+ "conversation.deleted": 410,
14808
+ "operation.conflict": 409,
14809
+ "source.not_allowed": 422,
14810
+ "source.invalid_revision": 422,
14811
+ "secret.unavailable": 503,
14812
+ "storage.capacity_exhausted": 507,
14813
+ "relay.body_too_large": 413,
14814
+ "relay.route_not_allowed": 422,
14815
+ "relay.deadline_exceeded": 504,
14816
+ "relay.upstream_error": 502,
14817
+ "internal.error": 500
14818
+ };
14819
+
14820
+ class ApiError extends Error {
14821
+ code;
14822
+ status;
14823
+ details;
14824
+ constructor(code, message, details) {
14825
+ super(message);
14826
+ this.code = code;
14827
+ this.status = ERROR_CODES[code];
14828
+ this.details = details;
14829
+ }
14830
+ }
14831
+ function errorEnvelope(code, message, requestId, details) {
14832
+ return { error: { code, message, request_id: requestId, ...details ? { details } : {} } };
14833
+ }
14806
14834
  // ../contracts/src/persistence.ts
14807
14835
  var DurationValueSchema = exports_external.string().refine(isDuration, { message: "expected a duration like 15s, 20m, or 2h" });
14808
14836
  var LAUNCH_MODES = ["create", "restore"];
@@ -14841,6 +14869,7 @@ var CheckpointPolicySchema = exports_external.object({
14841
14869
  var PersistenceSpecSchema = exports_external.object({
14842
14870
  mounts: exports_external.array(PersistenceMountSchema).max(16).default([]),
14843
14871
  conversationRestore: exports_external.enum(CONVERSATION_RESTORE_CAPABILITIES).default("filesystem_only"),
14872
+ conversationRetention: DurationValueSchema.default("168h"),
14844
14873
  sessionCompatibility: exports_external.string().min(1).max(128).optional(),
14845
14874
  checkpoint: CheckpointPolicySchema.prefault({})
14846
14875
  });
@@ -15016,6 +15045,7 @@ var WorkspaceResourceSchema = exports_external.object({
15016
15045
  agent_state: exports_external.enum(AGENT_STATES),
15017
15046
  change_cursor: exports_external.number().int().nonnegative(),
15018
15047
  provider_kind: exports_external.string().nullable(),
15048
+ provisioning_mode: exports_external.enum(["cold", "warm"]).nullable(),
15019
15049
  health: exports_external.record(exports_external.string(), exports_external.string()),
15020
15050
  created_at: exports_external.iso.datetime(),
15021
15051
  updated_at: exports_external.iso.datetime(),
@@ -15033,6 +15063,10 @@ var WorkspaceResourceSchema = exports_external.object({
15033
15063
  persistence: exports_external.object({
15034
15064
  enabled: exports_external.boolean(),
15035
15065
  conversation_restore: exports_external.enum(CONVERSATION_RESTORE_CAPABILITIES),
15066
+ conversation_resume: exports_external.object({
15067
+ status: exports_external.enum(["supported", "unsupported", "unknown"]),
15068
+ reason: exports_external.enum(["filesystem_only", "capability_unknown"]).nullable()
15069
+ }),
15036
15070
  latest_checkpoint_id: exports_external.uuid().nullable()
15037
15071
  }),
15038
15072
  outputs: exports_external.record(exports_external.string(), exports_external.unknown()),
@@ -15050,6 +15084,9 @@ var WorkspaceListQuerySchema = exports_external.object({
15050
15084
  external_id: exports_external.string().optional(),
15051
15085
  state: exports_external.enum(WORKSPACE_STATES).optional(),
15052
15086
  template: exports_external.string().optional(),
15087
+ metadata: exports_external.string().max(4096).optional(),
15088
+ created_after: exports_external.iso.datetime().optional(),
15089
+ created_before: exports_external.iso.datetime().optional(),
15053
15090
  limit: exports_external.coerce.number().int().positive().max(200).default(50),
15054
15091
  cursor: exports_external.string().optional()
15055
15092
  });
@@ -15073,6 +15110,7 @@ var EventEnvelopeSchema = exports_external.object({
15073
15110
  state: exports_external.enum(WORKSPACE_STATES),
15074
15111
  reason_code: exports_external.string().nullable(),
15075
15112
  agent_state: exports_external.enum(AGENT_STATES),
15113
+ provisioning_mode: exports_external.enum(["cold", "warm"]).nullable().default(null),
15076
15114
  change_cursor: exports_external.number().int().nonnegative(),
15077
15115
  failure: exports_external.object({
15078
15116
  reason_code: exports_external.enum(REASON_CODES),
@@ -15489,12 +15527,15 @@ function findRoute(snapshot, service, method, path) {
15489
15527
  // ../contracts/src/protocol.ts
15490
15528
  var LEGACY_PROTOCOL_VERSION = 1;
15491
15529
  var PROTOCOL_VERSION = 2;
15530
+ var POOL_PROTOCOL_VERSION = 3;
15492
15531
  var SUPPORTED_PROTOCOL_VERSIONS = [LEGACY_PROTOCOL_VERSION, PROTOCOL_VERSION];
15493
15532
  var MAX_FRAME_BYTES = 1048576;
15494
15533
  var HEADER_PROTOCOL = "x-pocketcoder-protocol";
15495
15534
  var HEADER_WORKSPACE = "x-pocketcoder-workspace";
15496
15535
  var HEADER_REGISTRATION = "x-pocketcoder-registration";
15497
15536
  var HEADER_RECONNECT = "x-pocketcoder-reconnect";
15537
+ var HEADER_POOL_RUNTIME = "x-pocketcoder-pool-runtime";
15538
+ var HEADER_POOL_ENROLLMENT = "x-pocketcoder-pool-enrollment";
15498
15539
  var EnvelopeBase = exports_external.object({
15499
15540
  v: exports_external.union([exports_external.literal(LEGACY_PROTOCOL_VERSION), exports_external.literal(PROTOCOL_VERSION)]),
15500
15541
  workspace_id: exports_external.uuid(),
@@ -15565,6 +15606,7 @@ var RestoreStatusPayload = exports_external.object({
15565
15606
  capability: exports_external.enum(CONVERSATION_RESTORE_CAPABILITIES),
15566
15607
  detail: exports_external.string().max(512).optional()
15567
15608
  });
15609
+ var ConversationMessagePayload = ConversationMessageInputSchema;
15568
15610
  var AgentFrameSchema = exports_external.discriminatedUnion("type", [
15569
15611
  EnvelopeBase.extend({ type: exports_external.literal("registered"), payload: RegisteredPayload }),
15570
15612
  EnvelopeBase.extend({ type: exports_external.literal("heartbeat"), payload: HeartbeatPayload }),
@@ -15577,7 +15619,11 @@ var AgentFrameSchema = exports_external.discriminatedUnion("type", [
15577
15619
  EnvelopeBase.extend({ type: exports_external.literal("source_resolved"), payload: SourceResolvedPayload }),
15578
15620
  EnvelopeBase.extend({ type: exports_external.literal("checkpoint_status"), payload: CheckpointStatusPayload }),
15579
15621
  EnvelopeBase.extend({ type: exports_external.literal("output_published"), payload: OutputPublishedPayload }),
15580
- EnvelopeBase.extend({ type: exports_external.literal("restore_status"), payload: RestoreStatusPayload })
15622
+ EnvelopeBase.extend({ type: exports_external.literal("restore_status"), payload: RestoreStatusPayload }),
15623
+ EnvelopeBase.extend({
15624
+ type: exports_external.literal("conversation_message"),
15625
+ payload: ConversationMessagePayload
15626
+ })
15581
15627
  ]);
15582
15628
  var ExecSpecSchema = exports_external.object({
15583
15629
  setup: exports_external.array(SetupStepSchema),
@@ -15670,6 +15716,27 @@ var ProviderInputSchema = exports_external.object({
15670
15716
  }).optional(),
15671
15717
  launch_input: exports_external.record(exports_external.string(), exports_external.unknown()).optional()
15672
15718
  });
15719
+ var PoolProviderInputSchema = exports_external.object({
15720
+ pool_runtime_id: exports_external.uuid(),
15721
+ server_url: exports_external.string(),
15722
+ enrollment_secret: exports_external.string().min(1),
15723
+ template_digest: exports_external.string(),
15724
+ template_name: exports_external.string(),
15725
+ template_version: exports_external.string()
15726
+ });
15727
+ var ProviderBootstrapInputSchema = exports_external.union([ProviderInputSchema, PoolProviderInputSchema]);
15728
+ var PoolRegisteredFrameSchema = exports_external.object({
15729
+ v: exports_external.literal(POOL_PROTOCOL_VERSION),
15730
+ type: exports_external.literal("pool_registered"),
15731
+ pool_runtime_id: exports_external.uuid(),
15732
+ template: exports_external.object({ name: exports_external.string(), version: exports_external.string(), digest: exports_external.string() }),
15733
+ agent_version: exports_external.string()
15734
+ });
15735
+ var LeaseAssignmentFrameSchema = exports_external.object({
15736
+ v: exports_external.literal(POOL_PROTOCOL_VERSION),
15737
+ type: exports_external.literal("lease_assignment"),
15738
+ input: ProviderInputSchema
15739
+ });
15673
15740
  // ../contracts/src/scopes.ts
15674
15741
  var SCOPES = [
15675
15742
  "templates:read",
@@ -15681,6 +15748,8 @@ var SCOPES = [
15681
15748
  "checkpoints:read",
15682
15749
  "checkpoints:delete",
15683
15750
  "outputs:read",
15751
+ "conversations:read",
15752
+ "conversations:delete",
15684
15753
  "services:relay",
15685
15754
  "logs:read",
15686
15755
  "admin"
@@ -31118,6 +31187,58 @@ ALTER TABLE "workspaces" ADD COLUMN "failure_log_tail" text;--> statement-breakp
31118
31187
  ALTER TABLE "workspaces" ADD COLUMN "failure_log_tail_truncated" boolean DEFAULT false NOT NULL;--> statement-breakpoint
31119
31188
  ALTER TABLE "workspaces" ADD COLUMN "failure_last_log_seq" bigint;`;
31120
31189
 
31190
+ // ../db/drizzle/20260803081501_warm-workspace-pool/migration.sql
31191
+ var migration_default4 = `CREATE TABLE "warm_pool_runtimes" (
31192
+ "id" uuid PRIMARY KEY,
31193
+ "template_id" uuid NOT NULL,
31194
+ "template_name" text NOT NULL,
31195
+ "template_version" text NOT NULL,
31196
+ "template_digest" text NOT NULL,
31197
+ "driver_kind" text NOT NULL,
31198
+ "eligibility_fingerprint" text NOT NULL,
31199
+ "state" text NOT NULL,
31200
+ "provider_ref" jsonb,
31201
+ "enrollment_digest" bytea,
31202
+ "enrollment_expires_at" timestamp with time zone,
31203
+ "workspace_id" uuid,
31204
+ "created_at" timestamp with time zone NOT NULL,
31205
+ "updated_at" timestamp with time zone NOT NULL,
31206
+ "ready_at" timestamp with time zone,
31207
+ "leased_at" timestamp with time zone,
31208
+ "failure_code" text
31209
+ );
31210
+ --> statement-breakpoint
31211
+ ALTER TABLE "workspaces" ADD COLUMN "provisioning_mode" text;--> statement-breakpoint
31212
+ CREATE INDEX "warm_pool_eligible" ON "warm_pool_runtimes" ("template_digest","driver_kind","eligibility_fingerprint","state","ready_at");--> statement-breakpoint
31213
+ CREATE UNIQUE INDEX "warm_pool_workspace_once" ON "warm_pool_runtimes" ("workspace_id");--> statement-breakpoint
31214
+ ALTER TABLE "warm_pool_runtimes" ADD CONSTRAINT "warm_pool_runtimes_template_id_templates_id_fkey" FOREIGN KEY ("template_id") REFERENCES "templates"("id");--> statement-breakpoint
31215
+ ALTER TABLE "warm_pool_runtimes" ADD CONSTRAINT "warm_pool_runtimes_workspace_id_workspaces_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id");`;
31216
+
31217
+ // ../db/drizzle/20260803092834_durable-conversations/migration.sql
31218
+ var migration_default5 = `CREATE TABLE "workspace_conversation_messages" (
31219
+ "workspace_id" uuid,
31220
+ "seq" bigint,
31221
+ "message_id" text NOT NULL,
31222
+ "role" text NOT NULL,
31223
+ "content" text NOT NULL,
31224
+ "occurred_at" timestamp with time zone NOT NULL,
31225
+ "metadata" jsonb DEFAULT '{}' NOT NULL,
31226
+ "created_at" timestamp with time zone NOT NULL,
31227
+ CONSTRAINT "workspace_conversation_messages_pkey" PRIMARY KEY("workspace_id","seq"),
31228
+ CONSTRAINT "workspace_conversation_messages_workspace_id_message_id_unique" UNIQUE("workspace_id","message_id")
31229
+ );
31230
+ --> statement-breakpoint
31231
+ CREATE TABLE "workspace_conversations" (
31232
+ "workspace_id" uuid PRIMARY KEY,
31233
+ "status" text NOT NULL,
31234
+ "expires_at" timestamp with time zone,
31235
+ "deleted_at" timestamp with time zone,
31236
+ "updated_at" timestamp with time zone NOT NULL
31237
+ );
31238
+ --> statement-breakpoint
31239
+ ALTER TABLE "workspace_conversation_messages" ADD CONSTRAINT "workspace_conversation_messages_workspace_id_workspaces_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id");--> statement-breakpoint
31240
+ ALTER TABLE "workspace_conversations" ADD CONSTRAINT "workspace_conversations_workspace_id_workspaces_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id");`;
31241
+
31121
31242
  // ../db/src/migrations.ts
31122
31243
  function migration(name2, folderMillis, source) {
31123
31244
  return {
@@ -31131,7 +31252,9 @@ function migration(name2, folderMillis, source) {
31131
31252
  var MIGRATIONS = [
31132
31253
  migration("20260730103433_initial", Date.UTC(2026, 6, 30, 10, 34, 33), migration_default),
31133
31254
  migration("20260730113243_regular_pretty_boy", Date.UTC(2026, 6, 30, 11, 32, 43), migration_default2),
31134
- migration("20260730132750_workspace-change-diagnostics", Date.UTC(2026, 6, 30, 13, 27, 50), migration_default3)
31255
+ migration("20260730132750_workspace-change-diagnostics", Date.UTC(2026, 6, 30, 13, 27, 50), migration_default3),
31256
+ migration("20260803081501_warm-workspace-pool", Date.UTC(2026, 7, 3, 8, 15, 1), migration_default4),
31257
+ migration("20260803092834_durable-conversations", Date.UTC(2026, 7, 3, 9, 28, 34), migration_default5)
31135
31258
  ];
31136
31259
 
31137
31260
  // ../db/src/schema.ts
@@ -31190,6 +31313,7 @@ var workspaces = pgTable("workspaces", {
31190
31313
  launchInput: jsonb("launch_input").$type(),
31191
31314
  providerKind: text2("provider_kind"),
31192
31315
  providerRef: jsonb("provider_ref").$type(),
31316
+ provisioningMode: text2("provisioning_mode"),
31193
31317
  registrationDigest: bytea2("registration_digest"),
31194
31318
  registrationExpiresAt: timestamptz("registration_expires_at"),
31195
31319
  reconnectDigest: bytea2("reconnect_digest"),
@@ -31219,6 +31343,28 @@ var workspaces = pgTable("workspaces", {
31219
31343
  index("workspaces_state_created").on(table.state, table.createdAt),
31220
31344
  index("workspaces_deadline").on(table.deadlineAt).where(sql`${table.state} NOT IN ${terminalStates}`)
31221
31345
  ]);
31346
+ var warmPoolRuntimes = pgTable("warm_pool_runtimes", {
31347
+ id: uuid3("id").primaryKey(),
31348
+ templateId: uuid3("template_id").notNull().references(() => templates.id),
31349
+ templateName: text2("template_name").notNull(),
31350
+ templateVersion: text2("template_version").notNull(),
31351
+ templateDigest: text2("template_digest").notNull(),
31352
+ driverKind: text2("driver_kind").notNull(),
31353
+ eligibilityFingerprint: text2("eligibility_fingerprint").notNull(),
31354
+ state: text2("state").notNull(),
31355
+ providerRef: jsonb("provider_ref").$type(),
31356
+ enrollmentDigest: bytea2("enrollment_digest"),
31357
+ enrollmentExpiresAt: timestamptz("enrollment_expires_at"),
31358
+ workspaceId: uuid3("workspace_id").references(() => workspaces.id),
31359
+ createdAt: timestamptz("created_at").notNull(),
31360
+ updatedAt: timestamptz("updated_at").notNull(),
31361
+ readyAt: timestamptz("ready_at"),
31362
+ leasedAt: timestamptz("leased_at"),
31363
+ failureCode: text2("failure_code")
31364
+ }, (table) => [
31365
+ index("warm_pool_eligible").on(table.templateDigest, table.driverKind, table.eligibilityFingerprint, table.state, table.readyAt),
31366
+ uniqueIndex("warm_pool_workspace_once").on(table.workspaceId)
31367
+ ]);
31222
31368
  var workspaceStorage = pgTable("workspace_storage", {
31223
31369
  id: uuid3("id").primaryKey(),
31224
31370
  workspaceId: uuid3("workspace_id").notNull().references(() => workspaces.id),
@@ -31294,6 +31440,26 @@ var workspaceOutputs = pgTable("workspace_outputs", {
31294
31440
  value: jsonb("value").$type().notNull(),
31295
31441
  occurredAt: timestamptz("occurred_at").notNull()
31296
31442
  }, (table) => [primaryKey({ columns: [table.workspaceId, table.seq] })]);
31443
+ var workspaceConversations = pgTable("workspace_conversations", {
31444
+ workspaceId: uuid3("workspace_id").primaryKey().references(() => workspaces.id),
31445
+ status: text2("status").notNull(),
31446
+ expiresAt: timestamptz("expires_at"),
31447
+ deletedAt: timestamptz("deleted_at"),
31448
+ updatedAt: timestamptz("updated_at").notNull()
31449
+ });
31450
+ var workspaceConversationMessages = pgTable("workspace_conversation_messages", {
31451
+ workspaceId: uuid3("workspace_id").notNull().references(() => workspaces.id),
31452
+ seq: bigint5("seq", { mode: "bigint" }).notNull(),
31453
+ messageId: text2("message_id").notNull(),
31454
+ role: text2("role").notNull(),
31455
+ content: text2("content").notNull(),
31456
+ occurredAt: timestamptz("occurred_at").notNull(),
31457
+ metadata: jsonb("metadata").$type().notNull().default({}),
31458
+ createdAt: timestamptz("created_at").notNull()
31459
+ }, (table) => [
31460
+ primaryKey({ columns: [table.workspaceId, table.seq] }),
31461
+ unique().on(table.workspaceId, table.messageId)
31462
+ ]);
31297
31463
  var workspaceStateHistory = pgTable("workspace_state_history", {
31298
31464
  id: uuid3("id").primaryKey(),
31299
31465
  workspaceId: uuid3("workspace_id").notNull().references(() => workspaces.id),
@@ -31406,7 +31572,7 @@ async function migrationStatus(sql2, schema) {
31406
31572
  });
31407
31573
  }
31408
31574
  // ../db/src/store.ts
31409
- import { randomUUID as randomUUID4 } from "crypto";
31575
+ import { randomUUID as randomUUID5 } from "crypto";
31410
31576
  // ../runtime-core/src/events.ts
31411
31577
  import { randomUUID as randomUUID2 } from "crypto";
31412
31578
  function buildEventEnvelope(row, occurredAt) {
@@ -31420,6 +31586,7 @@ function buildEventEnvelope(row, occurredAt) {
31420
31586
  state: row.state,
31421
31587
  reason_code: row.reasonCode,
31422
31588
  agent_state: row.agentState,
31589
+ provisioning_mode: row.provisioningMode,
31423
31590
  change_cursor: row.changeSeq,
31424
31591
  failure: row.state === "failed" && row.reasonCode ? {
31425
31592
  reason_code: row.reasonCode,
@@ -31896,7 +32063,26 @@ class Scheduler {
31896
32063
  return;
31897
32064
  case "provisioning":
31898
32065
  if (row.registrationExpiresAt && now >= row.registrationExpiresAt) {
31899
- await this.fail(row, "registration_timeout", now);
32066
+ if (row.provisioningMode === "warm" && row.launchAttempts < this.deps.limits.maxLaunchAttempts) {
32067
+ if (row.providerRef) {
32068
+ await this.deps.driver.stop(row.providerRef, 1).catch(() => {});
32069
+ await this.deps.driver.remove(row.providerRef).catch(() => {});
32070
+ }
32071
+ await this.deps.store.transition(row.id, {
32072
+ from: ["provisioning"],
32073
+ to: "queued",
32074
+ at: now,
32075
+ patch: {
32076
+ providerKind: null,
32077
+ providerRef: null,
32078
+ provisioningMode: null,
32079
+ registrationDigest: null,
32080
+ registrationExpiresAt: null
32081
+ }
32082
+ });
32083
+ } else {
32084
+ await this.fail(row, "registration_timeout", now);
32085
+ }
31900
32086
  }
31901
32087
  return;
31902
32088
  case "connected":
@@ -32085,19 +32271,8 @@ class Scheduler {
32085
32271
  const { store, driver, secrets } = this.deps;
32086
32272
  const now = this.now();
32087
32273
  const secret = secrets.generate();
32088
- const claimed = await store.transition(row.id, {
32089
- from: ["queued"],
32090
- to: "provisioning",
32091
- at: now,
32092
- patch: {
32093
- registrationDigest: secrets.digest(secret),
32094
- registrationExpiresAt: new Date(now.getTime() + this.timeoutMs(row, "start")),
32095
- launchAttempts: row.launchAttempts + 1
32096
- }
32097
- });
32098
- if (!claimed) {
32099
- return false;
32100
- }
32274
+ const registrationDigest = secrets.digest(secret);
32275
+ const registrationExpiresAt = new Date(now.getTime() + this.timeoutMs(row, "start"));
32101
32276
  const input = {
32102
32277
  workspace_id: row.id,
32103
32278
  server_url: this.deps.workspaceServerUrl,
@@ -32115,6 +32290,26 @@ class Scheduler {
32115
32290
  } : {},
32116
32291
  ...row.launchInput ? { launch_input: row.launchInput } : {}
32117
32292
  };
32293
+ if (this.deps.warmPool) {
32294
+ const hit = await this.deps.warmPool.tryLease(row, input, registrationDigest, registrationExpiresAt);
32295
+ if (hit)
32296
+ return true;
32297
+ if (this.deps.warmPool.missDecision(row) === "wait")
32298
+ return false;
32299
+ }
32300
+ const claimed = await store.transition(row.id, {
32301
+ from: ["queued"],
32302
+ to: "provisioning",
32303
+ at: now,
32304
+ patch: {
32305
+ provisioningMode: "cold",
32306
+ registrationDigest,
32307
+ registrationExpiresAt,
32308
+ launchAttempts: row.launchAttempts + 1
32309
+ }
32310
+ });
32311
+ if (!claimed)
32312
+ return false;
32118
32313
  try {
32119
32314
  const mounts = await this.prepareStorage(claimed);
32120
32315
  if (!this.deps.secretResolver && JSON.stringify(claimed.templateSnapshot.spec).includes('"secretRef:')) {
@@ -32242,9 +32437,275 @@ class Scheduler {
32242
32437
  await this.deps.store.updateOperation(operation.id, { state, reasonCode, completedAt: at, attemptCount: operation.attemptCount + 1 }, at);
32243
32438
  }
32244
32439
  }
32440
+ // ../runtime-core/src/warm-pool.ts
32441
+ import { randomUUID as randomUUID4 } from "crypto";
32442
+ function warmPoolFingerprint(template2, driverKind) {
32443
+ return digestOf({ template_digest: template2.digest, driver: driverKind });
32444
+ }
32445
+ function validateWarmPoolTemplate(template2) {
32446
+ if (template2.spec.persistence.mounts.length > 0) {
32447
+ throw new Error(`warm pool ${template2.name}@${template2.version} is ineligible: persistent mounts are not supported`);
32448
+ }
32449
+ if (JSON.stringify(template2.spec).includes('"secretRef:')) {
32450
+ throw new Error(`warm pool ${template2.name}@${template2.version} is ineligible: resolved secret mounts are not supported`);
32451
+ }
32452
+ }
32453
+ async function resolveWarmPools(store, entries, driverKind, globalLimit) {
32454
+ if (entries.reduce((sum3, entry) => sum3 + entry.minReady, 0) > globalLimit) {
32455
+ throw new Error("warm pool desired capacity exceeds POCKETCODER_MAX_ACTIVE_WORKSPACES");
32456
+ }
32457
+ const seen = new Set;
32458
+ const resolved = [];
32459
+ for (const entry of entries) {
32460
+ const template2 = await store.getTemplate(entry.template, entry.version);
32461
+ if (!template2 || template2.status === "retired") {
32462
+ throw new Error(`warm pool template not found: ${entry.template}${entry.version ? `@${entry.version}` : ""}`);
32463
+ }
32464
+ validateWarmPoolTemplate(template2);
32465
+ if (seen.has(template2.digest))
32466
+ throw new Error(`duplicate warm pool template digest: ${template2.digest}`);
32467
+ seen.add(template2.digest);
32468
+ resolved.push({
32469
+ ...entry,
32470
+ templateRow: template2,
32471
+ eligibilityFingerprint: warmPoolFingerprint(template2, driverKind)
32472
+ });
32473
+ }
32474
+ return resolved;
32475
+ }
32476
+
32477
+ class WarmPoolManager {
32478
+ deps;
32479
+ metrics = {
32480
+ warmHits: 0,
32481
+ misses: 0,
32482
+ leaseFailures: 0,
32483
+ replenishFailures: 0,
32484
+ staleCleanups: 0,
32485
+ leaseLatencyMsTotal: 0
32486
+ };
32487
+ constructor(deps) {
32488
+ this.deps = deps;
32489
+ }
32490
+ now() {
32491
+ return this.deps.now?.() ?? new Date;
32492
+ }
32493
+ poolFor(row) {
32494
+ return this.deps.pools.find((pool) => pool.templateRow.digest === row.templateDigest);
32495
+ }
32496
+ missDecision(row) {
32497
+ const pool = this.poolFor(row);
32498
+ if (!pool || pool.missPolicy === "cold")
32499
+ return "cold";
32500
+ return this.now().getTime() - row.createdAt.getTime() < pool.waitTimeoutMs ? "wait" : "cold";
32501
+ }
32502
+ async tryLease(row, input, registrationDigest, registrationExpiresAt) {
32503
+ const pool = this.poolFor(row);
32504
+ if (!pool)
32505
+ return false;
32506
+ const started = this.now();
32507
+ const claimed = await this.deps.store.claimWarmPoolRuntime({
32508
+ workspaceId: row.id,
32509
+ templateDigest: row.templateDigest,
32510
+ driverKind: this.deps.driver.kind,
32511
+ eligibilityFingerprint: pool.eligibilityFingerprint,
32512
+ registrationDigest,
32513
+ registrationExpiresAt,
32514
+ at: started
32515
+ });
32516
+ if (!claimed) {
32517
+ this.metrics.misses += 1;
32518
+ return false;
32519
+ }
32520
+ await this.deps.store.updateWarmPoolRuntime(claimed.runtime.id, { state: "leased" }, this.now());
32521
+ if (!this.deps.connections.assign(claimed.runtime.id, input)) {
32522
+ this.metrics.leaseFailures += 1;
32523
+ await this.destroy(claimed.runtime, "assignment_connection_lost");
32524
+ await this.deps.store.transition(row.id, {
32525
+ from: ["provisioning"],
32526
+ to: "queued",
32527
+ at: this.now(),
32528
+ patch: {
32529
+ providerKind: null,
32530
+ providerRef: null,
32531
+ provisioningMode: null,
32532
+ registrationDigest: null,
32533
+ registrationExpiresAt: null
32534
+ }
32535
+ });
32536
+ return false;
32537
+ }
32538
+ this.metrics.warmHits += 1;
32539
+ this.metrics.leaseLatencyMsTotal += this.now().getTime() - started.getTime();
32540
+ return true;
32541
+ }
32542
+ async markReady(runtimeId) {
32543
+ let row = await this.deps.store.getWarmPoolRuntime(runtimeId);
32544
+ for (let attempt = 0;row?.state === "provisioning" && !row.providerRef && attempt < 50; attempt += 1) {
32545
+ await new Promise((resolve) => setTimeout(resolve, 20));
32546
+ row = await this.deps.store.getWarmPoolRuntime(runtimeId);
32547
+ }
32548
+ if (row?.state !== "provisioning" || !row.providerRef)
32549
+ return false;
32550
+ const now = this.now();
32551
+ await this.deps.store.updateWarmPoolRuntime(runtimeId, {
32552
+ state: "ready",
32553
+ readyAt: now,
32554
+ enrollmentDigest: null,
32555
+ enrollmentExpiresAt: null
32556
+ }, now);
32557
+ this.deps.driver.cleanupWarmInput?.(runtimeId).catch(() => {});
32558
+ return true;
32559
+ }
32560
+ async disconnected(runtimeId) {
32561
+ const row = await this.deps.store.getWarmPoolRuntime(runtimeId);
32562
+ if (row && ["provisioning", "ready", "leasing"].includes(row.state)) {
32563
+ await this.destroy(row, "pool_agent_disconnected");
32564
+ }
32565
+ }
32566
+ async destroy(row, reason) {
32567
+ await this.deps.store.updateWarmPoolRuntime(row.id, { state: "draining", failureCode: reason }, this.now());
32568
+ this.deps.connections.close(row.id);
32569
+ if (row.providerRef) {
32570
+ await this.deps.driver.stop(row.providerRef, 1).catch(() => {});
32571
+ await this.deps.driver.remove(row.providerRef).catch(() => {});
32572
+ }
32573
+ await this.deps.store.updateWarmPoolRuntime(row.id, { state: "failed", failureCode: reason }, this.now());
32574
+ }
32575
+ async create(pool) {
32576
+ const now = this.now();
32577
+ const id = randomUUID4();
32578
+ const secret = this.deps.secrets.generate();
32579
+ const row = {
32580
+ id,
32581
+ templateId: pool.templateRow.id,
32582
+ templateName: pool.templateRow.name,
32583
+ templateVersion: pool.templateRow.version,
32584
+ templateDigest: pool.templateRow.digest,
32585
+ driverKind: this.deps.driver.kind,
32586
+ eligibilityFingerprint: pool.eligibilityFingerprint,
32587
+ state: "provisioning",
32588
+ providerRef: null,
32589
+ enrollmentDigest: this.deps.secrets.digest(secret),
32590
+ enrollmentExpiresAt: new Date(now.getTime() + parseDurationMs(pool.templateRow.spec.timeouts.start)),
32591
+ workspaceId: null,
32592
+ createdAt: now,
32593
+ updatedAt: now,
32594
+ readyAt: null,
32595
+ leasedAt: null,
32596
+ failureCode: null
32597
+ };
32598
+ await this.deps.store.insertWarmPoolRuntime(row);
32599
+ const input = {
32600
+ pool_runtime_id: id,
32601
+ server_url: this.deps.workspaceServerUrl,
32602
+ enrollment_secret: secret,
32603
+ template_digest: pool.templateRow.digest,
32604
+ template_name: pool.templateRow.name,
32605
+ template_version: pool.templateRow.version
32606
+ };
32607
+ try {
32608
+ const template2 = {
32609
+ name: pool.templateRow.name,
32610
+ version: pool.templateRow.version,
32611
+ digest: pool.templateRow.digest,
32612
+ spec: pool.templateRow.spec
32613
+ };
32614
+ const ref = await this.deps.driver.createWarm({ runtimeId: id, template: template2, input });
32615
+ await this.deps.store.updateWarmPoolRuntime(id, { providerRef: ref }, this.now());
32616
+ } catch (error51) {
32617
+ this.metrics.replenishFailures += 1;
32618
+ await this.deps.store.updateWarmPoolRuntime(id, { state: "failed", failureCode: "provider_create_failed" }, this.now());
32619
+ this.deps.onError?.(`warm-pool.create.${id}`, error51);
32620
+ }
32621
+ }
32622
+ async cleanupReason(row, pool, now) {
32623
+ if (!pool)
32624
+ return "pool_retired";
32625
+ if (["provisioning", "ready"].includes(row.state) && row.createdAt.getTime() + pool.maxWarmAgeMs <= now.getTime())
32626
+ return "max_warm_age";
32627
+ if (row.state === "provisioning" && row.enrollmentExpiresAt !== null && row.enrollmentExpiresAt <= now) {
32628
+ return "enrollment_timeout";
32629
+ }
32630
+ if (!row.workspaceId)
32631
+ return null;
32632
+ return (await this.deps.store.getWorkspace(row.workspaceId))?.terminalAt ? "workspace_ended" : null;
32633
+ }
32634
+ async reconcileRow(row, pool, now) {
32635
+ const reason = await this.cleanupReason(row, pool, now);
32636
+ if (reason) {
32637
+ if (row.state !== "failed") {
32638
+ this.metrics.staleCleanups += 1;
32639
+ await this.destroy(row, reason);
32640
+ }
32641
+ return;
32642
+ }
32643
+ if (!row.providerRef || ["failed", "draining"].includes(row.state))
32644
+ return;
32645
+ const state = await this.deps.driver.inspect(row.providerRef);
32646
+ if (!state.exists || !state.running)
32647
+ await this.destroy(row, "provider_lost");
32648
+ }
32649
+ async reconcile() {
32650
+ const now = this.now();
32651
+ const configured = new Map(this.deps.pools.map((pool) => [pool.templateRow.digest, pool]));
32652
+ const rows = await this.deps.store.listWarmPoolRuntimes();
32653
+ const known = new Map(rows.map((row) => [row.id, row]));
32654
+ for (const provider of await this.deps.driver.listWarm()) {
32655
+ const row = known.get(provider.runtimeId);
32656
+ if (!row || row.templateDigest !== provider.templateDigest || row.driverKind !== this.deps.driver.kind) {
32657
+ this.metrics.staleCleanups += 1;
32658
+ await this.deps.driver.stop(provider.ref, 1).catch(() => {});
32659
+ await this.deps.driver.remove(provider.ref).catch(() => {});
32660
+ continue;
32661
+ }
32662
+ if (!row.providerRef && row.state === "provisioning") {
32663
+ await this.deps.store.updateWarmPoolRuntime(row.id, { providerRef: provider.ref }, now);
32664
+ row.providerRef = provider.ref;
32665
+ }
32666
+ }
32667
+ for (const row of rows) {
32668
+ await this.reconcileRow(row, configured.get(row.templateDigest), now);
32669
+ }
32670
+ const fresh = await this.deps.store.listWarmPoolRuntimes();
32671
+ for (const pool of this.deps.pools) {
32672
+ const available = fresh.filter((row) => row.templateDigest === pool.templateRow.digest && ["provisioning", "ready"].includes(row.state)).length;
32673
+ for (let i = available;i < pool.minReady; i += 1)
32674
+ await this.create(pool);
32675
+ }
32676
+ }
32677
+ async inventory() {
32678
+ const rows = await this.deps.store.listWarmPoolRuntimes();
32679
+ const now = this.now();
32680
+ return {
32681
+ items: this.deps.pools.map((pool) => {
32682
+ const matching = rows.filter((row) => row.templateDigest === pool.templateRow.digest);
32683
+ const counts = {};
32684
+ for (const row of matching)
32685
+ counts[row.state] = (counts[row.state] ?? 0) + 1;
32686
+ const ready = matching.filter((row) => row.state === "ready" && row.readyAt).map((row) => row.readyAt);
32687
+ return {
32688
+ template: pool.templateRow.name,
32689
+ version: pool.templateRow.version,
32690
+ template_digest: pool.templateRow.digest,
32691
+ driver: this.deps.driver.kind,
32692
+ desired: pool.minReady,
32693
+ counts,
32694
+ oldest_ready_age_ms: ready.length ? now.getTime() - Math.min(...ready.map((date9) => date9.getTime())) : null
32695
+ };
32696
+ }),
32697
+ metrics: {
32698
+ ...this.metrics,
32699
+ average_lease_latency_ms: this.metrics.warmHits ? this.metrics.leaseLatencyMsTotal / this.metrics.warmHits : 0
32700
+ }
32701
+ };
32702
+ }
32703
+ }
32245
32704
  // ../db/src/store.ts
32246
32705
  var {SQL: SQL6 } = globalThis.Bun;
32247
32706
  var MAX_LOG_BYTES = 10 * 1024 * 1024;
32707
+ var MAX_CONVERSATION_BYTES = 50 * 1024 * 1024;
32708
+ var MAX_CONVERSATION_MESSAGES = 1e5;
32248
32709
  var CLAIM_LEASE_MS = 60000;
32249
32710
  function pgTextArray(items) {
32250
32711
  return `{${items.map((i) => `"${i.replace(/(["\\])/g, "\\$1")}"`).join(",")}}`;
@@ -32340,7 +32801,7 @@ class PostgresStore {
32340
32801
  (id, name, version, digest, description, spec, status, created_at)
32341
32802
  VALUES ($1, $2, $3, $4, $5, $6::jsonb, 'active', now())
32342
32803
  RETURNING *`, [
32343
- randomUUID4(),
32804
+ randomUUID5(),
32344
32805
  input.name,
32345
32806
  input.version,
32346
32807
  input.digest,
@@ -32364,6 +32825,127 @@ class PostgresStore {
32364
32825
  SET status = $3, retired_at = CASE WHEN $3 = 'retired' THEN now() ELSE NULL END
32365
32826
  WHERE name = $1 AND version = $2`, [name2, version2, status]);
32366
32827
  }
32828
+ warmPoolRuntimeFromRow(r) {
32829
+ return {
32830
+ id: String(r.id),
32831
+ templateId: String(r.template_id),
32832
+ templateName: String(r.template_name),
32833
+ templateVersion: String(r.template_version),
32834
+ templateDigest: String(r.template_digest),
32835
+ driverKind: String(r.driver_kind),
32836
+ eligibilityFingerprint: String(r.eligibility_fingerprint),
32837
+ state: r.state,
32838
+ providerRef: r.provider_ref == null ? null : asJson(r.provider_ref),
32839
+ enrollmentDigest: asBytes(r.enrollment_digest),
32840
+ enrollmentExpiresAt: asDateOrNull(r.enrollment_expires_at),
32841
+ workspaceId: r.workspace_id ?? null,
32842
+ createdAt: asDate(r.created_at),
32843
+ updatedAt: asDate(r.updated_at),
32844
+ readyAt: asDateOrNull(r.ready_at),
32845
+ leasedAt: asDateOrNull(r.leased_at),
32846
+ failureCode: r.failure_code ?? null
32847
+ };
32848
+ }
32849
+ async insertWarmPoolRuntime(row) {
32850
+ const rows = await this.sql.unsafe(`INSERT INTO ${this.t("warm_pool_runtimes")}
32851
+ (id, template_id, template_name, template_version, template_digest, driver_kind,
32852
+ eligibility_fingerprint, state, provider_ref, enrollment_digest,
32853
+ enrollment_expires_at, workspace_id, created_at, updated_at, ready_at,
32854
+ leased_at, failure_code)
32855
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10,$11,$12,$13,$14,$15,$16,$17)
32856
+ ON CONFLICT (id) DO NOTHING RETURNING *`, [
32857
+ row.id,
32858
+ row.templateId,
32859
+ row.templateName,
32860
+ row.templateVersion,
32861
+ row.templateDigest,
32862
+ row.driverKind,
32863
+ row.eligibilityFingerprint,
32864
+ row.state,
32865
+ row.providerRef == null ? null : JSON.stringify(row.providerRef),
32866
+ row.enrollmentDigest,
32867
+ row.enrollmentExpiresAt,
32868
+ row.workspaceId,
32869
+ row.createdAt,
32870
+ row.updatedAt,
32871
+ row.readyAt,
32872
+ row.leasedAt,
32873
+ row.failureCode
32874
+ ]);
32875
+ return rows[0] ? this.warmPoolRuntimeFromRow(rows[0]) : await this.getWarmPoolRuntime(row.id);
32876
+ }
32877
+ async getWarmPoolRuntime(id) {
32878
+ const rows = await this.sql.unsafe(`SELECT * FROM ${this.t("warm_pool_runtimes")} WHERE id = $1`, [id]);
32879
+ return rows[0] ? this.warmPoolRuntimeFromRow(rows[0]) : null;
32880
+ }
32881
+ async listWarmPoolRuntimes() {
32882
+ const rows = await this.sql.unsafe(`SELECT * FROM ${this.t("warm_pool_runtimes")} ORDER BY created_at`);
32883
+ return rows.map((row) => this.warmPoolRuntimeFromRow(row));
32884
+ }
32885
+ async updateWarmPoolRuntime(id, patch, at) {
32886
+ const columns = {
32887
+ state: "state",
32888
+ providerRef: "provider_ref",
32889
+ enrollmentDigest: "enrollment_digest",
32890
+ enrollmentExpiresAt: "enrollment_expires_at",
32891
+ workspaceId: "workspace_id",
32892
+ readyAt: "ready_at",
32893
+ leasedAt: "leased_at",
32894
+ failureCode: "failure_code"
32895
+ };
32896
+ const params = [id, at];
32897
+ const sets = ["updated_at = $2"];
32898
+ for (const [key, column] of Object.entries(columns)) {
32899
+ if (!(key in patch))
32900
+ continue;
32901
+ const value = patch[key] ?? null;
32902
+ params.push(key === "providerRef" && value !== null ? JSON.stringify(value) : value);
32903
+ sets.push(`${column} = $${params.length}${key === "providerRef" ? "::jsonb" : ""}`);
32904
+ }
32905
+ await this.sql.unsafe(`UPDATE ${this.t("warm_pool_runtimes")} SET ${sets.join(", ")} WHERE id = $1`, params);
32906
+ }
32907
+ async claimWarmPoolRuntime(claim) {
32908
+ const result = await this.sql.begin(async (tx) => {
32909
+ const workspaceRows = await tx.unsafe(`SELECT * FROM ${this.t("workspaces")} WHERE id = $1 FOR UPDATE`, [claim.workspaceId]);
32910
+ const current = workspaceRows[0] ? this.workspaceFromRow(workspaceRows[0]) : null;
32911
+ if (current?.state !== "queued")
32912
+ return null;
32913
+ const runtimeRows = await tx.unsafe(`SELECT * FROM ${this.t("warm_pool_runtimes")}
32914
+ WHERE template_digest = $1 AND driver_kind = $2 AND eligibility_fingerprint = $3
32915
+ AND state = 'ready' AND provider_ref IS NOT NULL
32916
+ ORDER BY ready_at FOR UPDATE SKIP LOCKED LIMIT 1`, [claim.templateDigest, claim.driverKind, claim.eligibilityFingerprint]);
32917
+ if (!runtimeRows[0])
32918
+ return null;
32919
+ const runtime = this.warmPoolRuntimeFromRow(runtimeRows[0]);
32920
+ const leasedRows = await tx.unsafe(`UPDATE ${this.t("warm_pool_runtimes")}
32921
+ SET state='leasing', workspace_id=$2, leased_at=$3, updated_at=$3
32922
+ WHERE id=$1 AND state='ready' RETURNING *`, [runtime.id, current.id, claim.at]);
32923
+ if (!leasedRows[0])
32924
+ return null;
32925
+ const updatedRows = await tx.unsafe(`UPDATE ${this.t("workspaces")}
32926
+ SET state='provisioning', provisioning_mode='warm', provider_kind=$2,
32927
+ provider_ref=$3::jsonb, registration_digest=$4,
32928
+ registration_expires_at=$5, launch_attempts=launch_attempts+1,
32929
+ updated_at=$6, change_seq=change_seq+1
32930
+ WHERE id=$1 AND state='queued' RETURNING *`, [
32931
+ current.id,
32932
+ runtime.driverKind,
32933
+ JSON.stringify(runtime.providerRef),
32934
+ claim.registrationDigest,
32935
+ claim.registrationExpiresAt,
32936
+ claim.at
32937
+ ]);
32938
+ if (!updatedRows[0])
32939
+ throw new Error("warm_pool.claim_workspace_race");
32940
+ const workspace2 = this.workspaceFromRow(updatedRows[0]);
32941
+ await this.appendHistoryTx(tx, workspace2, "queued", "provisioning", null, claim.at);
32942
+ await this.appendEventTx(tx, workspace2, claim.at);
32943
+ return { runtime: this.warmPoolRuntimeFromRow(leasedRows[0]), workspace: workspace2 };
32944
+ });
32945
+ if (result)
32946
+ this.notifyWorkspaceChange(claim.workspaceId);
32947
+ return result;
32948
+ }
32367
32949
  principalFromRow(r) {
32368
32950
  return {
32369
32951
  id: String(r.id),
@@ -32376,7 +32958,7 @@ class PostgresStore {
32376
32958
  }
32377
32959
  async createPrincipal(name2, scopes2, templateNames) {
32378
32960
  const rows = await this.sql.unsafe(`INSERT INTO ${this.t("principals")} (id, name, scopes, template_names, created_at)
32379
- VALUES ($1, $2, $3::text[], $4::text[], now()) RETURNING *`, [randomUUID4(), name2, pgTextArray(scopes2), pgTextArray(templateNames)]);
32961
+ VALUES ($1, $2, $3::text[], $4::text[], now()) RETURNING *`, [randomUUID5(), name2, pgTextArray(scopes2), pgTextArray(templateNames)]);
32380
32962
  return this.principalFromRow(rows[0]);
32381
32963
  }
32382
32964
  async getPrincipalByName(name2) {
@@ -32477,6 +33059,7 @@ class PostgresStore {
32477
33059
  launchInput: r.launch_input == null ? null : asJson(r.launch_input),
32478
33060
  providerKind: r.provider_kind ?? null,
32479
33061
  providerRef: r.provider_ref == null ? null : asJson(r.provider_ref),
33062
+ provisioningMode: r.provisioning_mode ?? null,
32480
33063
  registrationDigest: asBytes(r.registration_digest),
32481
33064
  registrationExpiresAt: asDateOrNull(r.registration_expires_at),
32482
33065
  reconnectDigest: asBytes(r.reconnect_digest),
@@ -32586,13 +33169,26 @@ class PostgresStore {
32586
33169
  params.push(filter.template);
32587
33170
  clauses.push(`template_name = $${params.length}`);
32588
33171
  }
33172
+ if (filter.metadata) {
33173
+ params.push(JSON.stringify(filter.metadata));
33174
+ clauses.push(`metadata @> $${params.length}::jsonb`);
33175
+ }
33176
+ if (filter.createdAfter) {
33177
+ params.push(filter.createdAfter);
33178
+ clauses.push(`created_at >= $${params.length}`);
33179
+ }
33180
+ if (filter.createdBefore) {
33181
+ params.push(filter.createdBefore);
33182
+ clauses.push(`created_at < $${params.length}`);
33183
+ }
32589
33184
  if (filter.cursor) {
32590
33185
  params.push(filter.cursor);
32591
- clauses.push(`created_at < (SELECT created_at FROM ${this.t("workspaces")} WHERE id = $${params.length})`);
33186
+ clauses.push(`(created_at, id) < (SELECT created_at, id FROM ${this.t("workspaces")}
33187
+ WHERE id = $${params.length} AND principal_id = $1)`);
32592
33188
  }
32593
33189
  params.push(filter.limit);
32594
33190
  const rows = await this.sql.unsafe(`SELECT * FROM ${this.t("workspaces")} WHERE ${clauses.join(" AND ")}
32595
- ORDER BY created_at DESC LIMIT $${params.length}`, params);
33191
+ ORDER BY created_at DESC, id DESC LIMIT $${params.length}`, params);
32596
33192
  return rows.map((r) => this.workspaceFromRow(r));
32597
33193
  }
32598
33194
  async listQueued(limit) {
@@ -32627,6 +33223,7 @@ class PostgresStore {
32627
33223
  launchInput: "launch_input",
32628
33224
  providerKind: "provider_kind",
32629
33225
  providerRef: "provider_ref",
33226
+ provisioningMode: "provisioning_mode",
32630
33227
  registrationDigest: "registration_digest",
32631
33228
  registrationExpiresAt: "registration_expires_at",
32632
33229
  reconnectDigest: "reconnect_digest",
@@ -32642,6 +33239,7 @@ class PostgresStore {
32642
33239
  failureLogTailTruncated: "failure_log_tail_truncated",
32643
33240
  failureLastLogSeq: "failure_last_log_seq",
32644
33241
  resolvedSource: "resolved_source",
33242
+ persistenceCapability: "persistence_capability",
32645
33243
  latestCheckpointId: "latest_checkpoint_id",
32646
33244
  outputs: "outputs"
32647
33245
  };
@@ -32661,6 +33259,7 @@ class PostgresStore {
32661
33259
  "failureLogTailTruncated",
32662
33260
  "failureLastLogSeq",
32663
33261
  "resolvedSource",
33262
+ "persistenceCapability",
32664
33263
  "latestCheckpointId",
32665
33264
  "outputs"
32666
33265
  ]);
@@ -32757,6 +33356,15 @@ class PostgresStore {
32757
33356
  const workspace3 = this.workspaceFromRow(updated[0]);
32758
33357
  await this.appendHistoryTx(tx, workspace3, current.state, req.to, workspace3.reasonCode, req.at);
32759
33358
  await this.appendEventTx(tx, workspace3, req.at);
33359
+ if (isTerminal(req.to)) {
33360
+ const expiresAt = new Date(req.at.getTime() + parseDurationMs(workspace3.templateSnapshot.spec.persistence.conversationRetention));
33361
+ await tx.unsafe(`INSERT INTO ${this.t("workspace_conversations")}
33362
+ (workspace_id, status, expires_at, deleted_at, updated_at)
33363
+ VALUES ($1, 'retained', $2, NULL, $3)
33364
+ ON CONFLICT (workspace_id) DO UPDATE
33365
+ SET expires_at = EXCLUDED.expires_at, updated_at = EXCLUDED.updated_at
33366
+ WHERE ${this.t("workspace_conversations")}.status <> 'deleted'`, [workspace3.id, expiresAt, req.at]);
33367
+ }
32760
33368
  return workspace3;
32761
33369
  });
32762
33370
  if (workspace2)
@@ -33124,7 +33732,7 @@ class PostgresStore {
33124
33732
  async appendHistoryTx(tx, row, from, to, reason, at) {
33125
33733
  await tx.unsafe(`INSERT INTO ${this.t("workspace_state_history")}
33126
33734
  (id, workspace_id, from_state, to_state, reason_code, occurred_at)
33127
- VALUES ($1, $2, $3, $4, $5, $6)`, [randomUUID4(), row.id, from, to, reason, at]);
33735
+ VALUES ($1, $2, $3, $4, $5, $6)`, [randomUUID5(), row.id, from, to, reason, at]);
33128
33736
  }
33129
33737
  async appendEventTx(tx, row, at) {
33130
33738
  const payload = buildEventEnvelope(row, at);
@@ -33190,6 +33798,104 @@ class PostgresStore {
33190
33798
  lastSeq
33191
33799
  };
33192
33800
  }
33801
+ conversationMessageFromRow(row) {
33802
+ return {
33803
+ workspaceId: String(row.workspace_id),
33804
+ seq: Number(row.seq),
33805
+ messageId: String(row.message_id),
33806
+ role: row.role,
33807
+ content: String(row.content),
33808
+ occurredAt: asDate(row.occurred_at),
33809
+ metadata: asJson(row.metadata),
33810
+ createdAt: asDate(row.created_at)
33811
+ };
33812
+ }
33813
+ async appendConversationMessage(input) {
33814
+ return await this.sql.begin(async (tx) => {
33815
+ await tx.unsafe("SELECT pg_advisory_xact_lock(hashtextextended($1, 7081))", [
33816
+ input.workspaceId
33817
+ ]);
33818
+ const states = await tx.unsafe(`SELECT status FROM ${this.t("workspace_conversations")} WHERE workspace_id = $1 FOR UPDATE`, [input.workspaceId]);
33819
+ if (states[0]?.status === "deleted")
33820
+ throw new Error("conversation.deleted");
33821
+ const existing = await tx.unsafe(`SELECT * FROM ${this.t("workspace_conversation_messages")}
33822
+ WHERE workspace_id = $1 AND message_id = $2`, [input.workspaceId, input.messageId]);
33823
+ if (existing[0]) {
33824
+ return { message: this.conversationMessageFromRow(existing[0]), created: false };
33825
+ }
33826
+ await tx.unsafe(`INSERT INTO ${this.t("workspace_conversations")}
33827
+ (workspace_id, status, expires_at, deleted_at, updated_at)
33828
+ VALUES ($1, 'retained', NULL, NULL, $2)
33829
+ ON CONFLICT (workspace_id) DO NOTHING`, [input.workspaceId, input.createdAt]);
33830
+ const stats = await tx.unsafe(`SELECT COALESCE(MAX(seq), 0)::bigint AS max_seq,
33831
+ COUNT(*)::bigint AS message_count,
33832
+ COALESCE(SUM(octet_length(content) + octet_length(metadata::text)), 0)::bigint AS bytes
33833
+ FROM ${this.t("workspace_conversation_messages")} WHERE workspace_id = $1`, [input.workspaceId]);
33834
+ const inputBytes = Buffer.byteLength(input.content) + Buffer.byteLength(JSON.stringify(input.metadata));
33835
+ if (Number(stats[0]?.message_count ?? 0) >= MAX_CONVERSATION_MESSAGES || Number(stats[0]?.bytes ?? 0) + inputBytes > MAX_CONVERSATION_BYTES) {
33836
+ throw new Error("conversation.quota_exceeded");
33837
+ }
33838
+ const seq = Number(stats[0]?.max_seq ?? 0) + 1;
33839
+ const inserted = await tx.unsafe(`INSERT INTO ${this.t("workspace_conversation_messages")}
33840
+ (workspace_id, seq, message_id, role, content, occurred_at, metadata, created_at)
33841
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8) RETURNING *`, [
33842
+ input.workspaceId,
33843
+ seq,
33844
+ input.messageId,
33845
+ input.role,
33846
+ input.content,
33847
+ input.occurredAt,
33848
+ JSON.stringify(input.metadata),
33849
+ input.createdAt
33850
+ ]);
33851
+ return { message: this.conversationMessageFromRow(inserted[0]), created: true };
33852
+ });
33853
+ }
33854
+ async readConversation(workspaceId, afterSeq, limit) {
33855
+ const rows = await this.sql.unsafe(`SELECT * FROM ${this.t("workspace_conversation_messages")}
33856
+ WHERE workspace_id = $1 AND seq > $2 ORDER BY seq ASC LIMIT $3`, [workspaceId, afterSeq, limit]);
33857
+ return rows.map((row) => this.conversationMessageFromRow(row));
33858
+ }
33859
+ async getConversationState(workspaceId) {
33860
+ const rows = await this.sql.unsafe(`SELECT * FROM ${this.t("workspace_conversations")} WHERE workspace_id = $1`, [workspaceId]);
33861
+ const row = rows[0];
33862
+ return row ? {
33863
+ workspaceId: String(row.workspace_id),
33864
+ status: row.status,
33865
+ expiresAt: asDateOrNull(row.expires_at),
33866
+ deletedAt: asDateOrNull(row.deleted_at),
33867
+ updatedAt: asDate(row.updated_at)
33868
+ } : null;
33869
+ }
33870
+ async setConversationExpiry(workspaceId, expiresAt, at) {
33871
+ await this.sql.unsafe(`INSERT INTO ${this.t("workspace_conversations")}
33872
+ (workspace_id, status, expires_at, deleted_at, updated_at)
33873
+ VALUES ($1, 'retained', $2, NULL, $3)
33874
+ ON CONFLICT (workspace_id) DO UPDATE
33875
+ SET expires_at = EXCLUDED.expires_at, updated_at = EXCLUDED.updated_at
33876
+ WHERE ${this.t("workspace_conversations")}.status <> 'deleted'`, [workspaceId, expiresAt, at]);
33877
+ }
33878
+ async deleteConversation(workspaceId, at) {
33879
+ await this.sql.begin(async (tx) => {
33880
+ await tx.unsafe(`DELETE FROM ${this.t("workspace_conversation_messages")} WHERE workspace_id = $1`, [workspaceId]);
33881
+ await tx.unsafe(`INSERT INTO ${this.t("workspace_conversations")}
33882
+ (workspace_id, status, expires_at, deleted_at, updated_at)
33883
+ VALUES ($1, 'deleted', NULL, $2, $2)
33884
+ ON CONFLICT (workspace_id) DO UPDATE
33885
+ SET status = 'deleted', expires_at = NULL, deleted_at = EXCLUDED.deleted_at,
33886
+ updated_at = EXCLUDED.updated_at`, [workspaceId, at]);
33887
+ });
33888
+ }
33889
+ async pruneExpiredConversations(at) {
33890
+ const rows = await this.sql.unsafe(`DELETE FROM ${this.t("workspace_conversation_messages")} AS messages
33891
+ USING ${this.t("workspace_conversations")} AS conversations
33892
+ WHERE messages.workspace_id = conversations.workspace_id
33893
+ AND conversations.status = 'retained'
33894
+ AND conversations.expires_at IS NOT NULL
33895
+ AND conversations.expires_at <= $1
33896
+ RETURNING messages.workspace_id`, [at]);
33897
+ return new Set(rows.map((row) => String(row.workspace_id))).size;
33898
+ }
33193
33899
  outboxFromRow(r) {
33194
33900
  return {
33195
33901
  id: String(r.id),
@@ -33227,7 +33933,7 @@ class PostgresStore {
33227
33933
  async appendEvent(workspaceId, eventType, payload, at) {
33228
33934
  await this.sql.unsafe(`INSERT INTO ${this.t("event_outbox")}
33229
33935
  (id, workspace_id, event_type, payload, occurred_at, next_attempt_at)
33230
- VALUES ($1, $2, $3, $4::jsonb, $5, $5)`, [randomUUID4(), workspaceId, eventType, JSON.stringify(payload), at]);
33936
+ VALUES ($1, $2, $3, $4::jsonb, $5, $5)`, [randomUUID5(), workspaceId, eventType, JSON.stringify(payload), at]);
33231
33937
  }
33232
33938
  }
33233
33939
  // src/index.ts
@@ -38085,7 +38791,7 @@ var yargs_default = Yargs;
38085
38791
 
38086
38792
  // src/server-process.ts
38087
38793
  import { spawn, spawnSync } from "child_process";
38088
- import { createHash as createHash5, randomUUID as randomUUID10 } from "crypto";
38794
+ import { createHash as createHash5, randomUUID as randomUUID12 } from "crypto";
38089
38795
  import {
38090
38796
  closeSync,
38091
38797
  existsSync,
@@ -38103,10 +38809,10 @@ import { dirname as dirname5, join as join4, resolve as resolve7 } from "path";
38103
38809
  import { randomBytes as randomBytes2 } from "crypto";
38104
38810
 
38105
38811
  // ../../apps/pocketcoder-server/src/persistence.ts
38106
- import { randomUUID as randomUUID6 } from "crypto";
38812
+ import { randomUUID as randomUUID7 } from "crypto";
38107
38813
 
38108
38814
  // ../../apps/pocketcoder-server/src/service.ts
38109
- import { randomUUID as randomUUID5 } from "crypto";
38815
+ import { randomUUID as randomUUID6 } from "crypto";
38110
38816
  function templateAuthorized(principal, templateName) {
38111
38817
  return principal.templateNames.includes("*") || principal.templateNames.includes(templateName);
38112
38818
  }
@@ -38124,6 +38830,7 @@ function toResource(row) {
38124
38830
  agent_state: row.agentState,
38125
38831
  change_cursor: row.changeSeq,
38126
38832
  provider_kind: row.providerKind,
38833
+ provisioning_mode: row.provisioningMode,
38127
38834
  health: row.health,
38128
38835
  created_at: row.createdAt.toISOString(),
38129
38836
  updated_at: row.updatedAt.toISOString(),
@@ -38143,6 +38850,7 @@ function toResource(row) {
38143
38850
  persistence: {
38144
38851
  enabled: row.templateSnapshot.spec.persistence.mounts.length > 0,
38145
38852
  conversation_restore: row.persistenceCapability,
38853
+ conversation_resume: row.persistenceCapability === "supported" ? { status: "supported", reason: null } : row.persistenceCapability === "filesystem_only" ? { status: "unsupported", reason: "filesystem_only" } : { status: "unknown", reason: "capability_unknown" },
38146
38854
  latest_checkpoint_id: row.latestCheckpointId
38147
38855
  },
38148
38856
  outputs: row.outputs,
@@ -38204,7 +38912,7 @@ class WorkspaceService {
38204
38912
  }
38205
38913
  const now = this.now();
38206
38914
  const result = await store2.insertWorkspace({
38207
- id: randomUUID5(),
38915
+ id: randomUUID6(),
38208
38916
  principalId: principal.id,
38209
38917
  externalId: body.external_id,
38210
38918
  idempotencyKey,
@@ -38361,8 +39069,8 @@ class PersistenceService {
38361
39069
  }
38362
39070
  this.storageDriver();
38363
39071
  const requestDigest = digestOf({ workspace_id: workspaceId, ...body });
38364
- const checkpointId = randomUUID6();
38365
- const operationId = randomUUID6();
39072
+ const checkpointId = randomUUID7();
39073
+ const operationId = randomUUID7();
38366
39074
  const now = this.now();
38367
39075
  const operationResult = await this.deps.store.insertOperation({
38368
39076
  id: operationId,
@@ -38484,6 +39192,7 @@ class PersistenceService {
38484
39192
  }
38485
39193
  async pruneExpired() {
38486
39194
  const now = this.now();
39195
+ const transcriptsDeleted = await this.deps.store.pruneExpiredConversations(now);
38487
39196
  let deleted = 0;
38488
39197
  let skipped = 0;
38489
39198
  for (const principal of await this.deps.store.listPrincipals()) {
@@ -38505,7 +39214,7 @@ class PersistenceService {
38505
39214
  }
38506
39215
  }
38507
39216
  }
38508
- return { deleted, skipped };
39217
+ return { deleted, skipped, transcriptsDeleted };
38509
39218
  }
38510
39219
  async stopForSnapshot(workspace2, operationId) {
38511
39220
  const hook = workspace2.templateSnapshot.spec.checkpointHook;
@@ -38530,6 +39239,7 @@ class PersistenceService {
38530
39239
  async completePreserve(workspace2, checkpoint, storage, operationId, result, quiesced) {
38531
39240
  const { store: store2, driver: driver2, hub } = this.deps;
38532
39241
  const readyAt = this.now();
39242
+ const conversationRestore = quiesced ? workspace2.persistenceCapability : "filesystem_only";
38533
39243
  await store2.updateCheckpoint(checkpoint.id, {
38534
39244
  state: "ready",
38535
39245
  providerKind: this.storageDriver().kind,
@@ -38539,7 +39249,7 @@ class PersistenceService {
38539
39249
  logicalBytes: result.manifest.logical_bytes,
38540
39250
  storedBytes: result.storedBytes,
38541
39251
  fileCount: result.manifest.file_count,
38542
- conversationRestore: quiesced ? workspace2.persistenceCapability : "filesystem_only",
39252
+ conversationRestore,
38543
39253
  readyAt
38544
39254
  }, readyAt);
38545
39255
  const readyCheckpoint = await store2.getCheckpoint(checkpoint.id);
@@ -38551,7 +39261,7 @@ class PersistenceService {
38551
39261
  fileCount: result.manifest.file_count,
38552
39262
  retainedUntil: checkpoint.expiresAt
38553
39263
  }, readyAt);
38554
- await store2.updateWorkspace(workspace2.id, { latestCheckpointId: checkpoint.id }, readyAt);
39264
+ await store2.updateWorkspace(workspace2.id, { latestCheckpointId: checkpoint.id, persistenceCapability: conversationRestore }, readyAt);
38555
39265
  if (workspace2.providerRef) {
38556
39266
  await driver2.remove({
38557
39267
  kind: workspace2.providerKind ?? "",
@@ -38634,9 +39344,9 @@ class PersistenceService {
38634
39344
  const now = this.now();
38635
39345
  const requestDigest = digestOf({ checkpoint_id: checkpointId, ...body });
38636
39346
  await this.assertOperationCapacity(principal.id, "restore", idempotencyKey);
38637
- const resultWorkspaceId = randomUUID6();
39347
+ const resultWorkspaceId = randomUUID7();
38638
39348
  const operationResult = await this.deps.store.insertOperation({
38639
- id: randomUUID6(),
39349
+ id: randomUUID7(),
38640
39350
  principalId: principal.id,
38641
39351
  kind: "restore",
38642
39352
  state: "pending",
@@ -38701,7 +39411,7 @@ class PersistenceService {
38701
39411
  throw new ApiError("idempotency.conflict", "The requested external_id is already active.");
38702
39412
  }
38703
39413
  await this.deps.store.appendEvent(checkpoint.workspaceId, "workspace.restore_queued", {
38704
- id: randomUUID6(),
39414
+ id: randomUUID7(),
38705
39415
  type: "workspace.restore_queued",
38706
39416
  occurred_at: now.toISOString(),
38707
39417
  origin_workspace_id: checkpoint.workspaceId,
@@ -38722,7 +39432,7 @@ class PersistenceService {
38722
39432
  const now = this.now();
38723
39433
  await this.assertOperationCapacity(principal.id, "verify", idempotencyKey);
38724
39434
  const inserted = await this.deps.store.insertOperation({
38725
- id: randomUUID6(),
39435
+ id: randomUUID7(),
38726
39436
  principalId: principal.id,
38727
39437
  kind: "verify",
38728
39438
  state: "running",
@@ -38762,7 +39472,7 @@ class PersistenceService {
38762
39472
  const now = this.now();
38763
39473
  await this.assertOperationCapacity(principal.id, "delete", idempotencyKey);
38764
39474
  const inserted = await this.deps.store.insertOperation({
38765
- id: randomUUID6(),
39475
+ id: randomUUID7(),
38766
39476
  principalId: principal.id,
38767
39477
  kind: "delete",
38768
39478
  state: "running",
@@ -38846,7 +39556,7 @@ class PersistenceService {
38846
39556
  });
38847
39557
  const at = this.now();
38848
39558
  await this.deps.store.appendEvent(workspaceId, "workspace.output_published", {
38849
- id: randomUUID6(),
39559
+ id: randomUUID7(),
38850
39560
  type: "workspace.output_published",
38851
39561
  occurred_at: at.toISOString(),
38852
39562
  workspace_id: workspaceId,
@@ -38890,7 +39600,7 @@ class PersistenceService {
38890
39600
  async emitCheckpointEvent(type, row) {
38891
39601
  const at = this.now();
38892
39602
  await this.deps.store.appendEvent(row.workspaceId, type, {
38893
- id: randomUUID6(),
39603
+ id: randomUUID7(),
38894
39604
  type,
38895
39605
  occurred_at: at.toISOString(),
38896
39606
  checkpoint: {
@@ -38907,6 +39617,50 @@ class PersistenceService {
38907
39617
  }
38908
39618
 
38909
39619
  // ../../apps/pocketcoder-server/src/config.ts
39620
+ function resolveWarmPools2(env2) {
39621
+ const raw = env2.POCKETCODER_WARM_POOLS;
39622
+ if (!raw)
39623
+ return [];
39624
+ let value;
39625
+ try {
39626
+ value = JSON.parse(raw);
39627
+ } catch {
39628
+ throw new Error("POCKETCODER_WARM_POOLS must be valid JSON");
39629
+ }
39630
+ if (!Array.isArray(value))
39631
+ throw new Error("POCKETCODER_WARM_POOLS must be a JSON array");
39632
+ return value.map((item, index2) => {
39633
+ if (!item || typeof item !== "object")
39634
+ throw new Error(`POCKETCODER_WARM_POOLS[${index2}] must be an object`);
39635
+ const entry = item;
39636
+ if (typeof entry.template !== "string" || !entry.template)
39637
+ throw new Error(`POCKETCODER_WARM_POOLS[${index2}].template is required`);
39638
+ const minReady = entry.min_ready === undefined ? 1 : Number(entry.min_ready);
39639
+ if (!Number.isInteger(minReady) || minReady <= 0)
39640
+ throw new Error(`POCKETCODER_WARM_POOLS[${index2}].min_ready must be a positive integer`);
39641
+ const missPolicy = entry.miss_policy ?? "cold";
39642
+ if (missPolicy !== "cold" && missPolicy !== "wait")
39643
+ throw new Error(`POCKETCODER_WARM_POOLS[${index2}].miss_policy must be cold or wait`);
39644
+ const duration4 = (key, fallback) => {
39645
+ const candidate = entry[key] ?? fallback;
39646
+ if (typeof candidate !== "string")
39647
+ throw new Error(`POCKETCODER_WARM_POOLS[${index2}].${key} must be a duration`);
39648
+ try {
39649
+ return parseDurationMs(candidate);
39650
+ } catch {
39651
+ throw new Error(`POCKETCODER_WARM_POOLS[${index2}].${key} must be a duration`);
39652
+ }
39653
+ };
39654
+ return {
39655
+ template: entry.template,
39656
+ ...typeof entry.version === "string" ? { version: entry.version } : {},
39657
+ minReady,
39658
+ maxWarmAgeMs: duration4("max_warm_age", "15m"),
39659
+ missPolicy,
39660
+ waitTimeoutMs: duration4("wait_timeout", "5s")
39661
+ };
39662
+ });
39663
+ }
38910
39664
  function intEnv(env2, key, fallback) {
38911
39665
  const raw = env2[key];
38912
39666
  if (!raw)
@@ -39030,7 +39784,8 @@ function loadConfig(env2 = process.env) {
39030
39784
  limits: resolveAdmissionLimits(env2),
39031
39785
  schedulerIntervalMs: intEnv(env2, "POCKETCODER_SCHEDULER_INTERVAL_MS", 1000),
39032
39786
  outboxIntervalMs: intEnv(env2, "POCKETCODER_OUTBOX_INTERVAL_MS", 1000),
39033
- persistenceLimits: resolvePersistenceLimits(env2)
39787
+ persistenceLimits: resolvePersistenceLimits(env2),
39788
+ warmPools: resolveWarmPools2(env2)
39034
39789
  };
39035
39790
  }
39036
39791
 
@@ -39040,6 +39795,7 @@ import { tmpdir } from "os";
39040
39795
  import { join as join2 } from "path";
39041
39796
  var WORKSPACE_LABEL = "pocketcoder.workspace";
39042
39797
  var DIGEST_LABEL = "pocketcoder.template-digest";
39798
+ var POOL_RUNTIME_LABEL = "pocketcoder.pool-runtime";
39043
39799
  async function run(bin, args) {
39044
39800
  const proc = Bun.spawn([bin, ...args], { stdout: "pipe", stderr: "pipe" });
39045
39801
  const [stdout, stderr, code] = await Promise.all([
@@ -39148,6 +39904,56 @@ class DockerDriver {
39148
39904
  throw err;
39149
39905
  }
39150
39906
  }
39907
+ async createWarm(launch) {
39908
+ const spec = launch.template.spec;
39909
+ await mkdir(this.opts.inputDir, { recursive: true, mode: 448 });
39910
+ await chmod(this.opts.inputDir, 448);
39911
+ const inputFile = this.inputPath(`pool-${launch.runtimeId}`);
39912
+ await writeFile2(inputFile, JSON.stringify(launch.input), { mode: 420 });
39913
+ const name2 = `pocketcoder-pool-${launch.runtimeId}`;
39914
+ const args = [
39915
+ "run",
39916
+ "--detach",
39917
+ "--name",
39918
+ name2,
39919
+ "--label",
39920
+ `${POOL_RUNTIME_LABEL}=${launch.runtimeId}`,
39921
+ "--label",
39922
+ `${DIGEST_LABEL}=${launch.template.digest}`,
39923
+ "--restart=no",
39924
+ "--user",
39925
+ `${spec.security.uid}:${spec.security.gid}`,
39926
+ "--security-opt",
39927
+ "no-new-privileges",
39928
+ "--cpus",
39929
+ spec.resources.cpu.endsWith("m") ? String(Number(spec.resources.cpu.slice(0, -1)) / 1000) : spec.resources.cpu,
39930
+ "--memory",
39931
+ spec.resources.memory.replace("Mi", "m").replace("Gi", "g"),
39932
+ "-v",
39933
+ `${inputFile}:/run/pocketcoder/input:ro`
39934
+ ];
39935
+ for (const cap of spec.security.dropCapabilities)
39936
+ args.push("--cap-drop", cap);
39937
+ if (spec.security.readOnlyRoot)
39938
+ args.push("--read-only");
39939
+ for (const path of spec.security.writableMemoryPaths) {
39940
+ args.push("--tmpfs", `${path}:rw,noexec,nosuid,size=256m,uid=${spec.security.uid},gid=${spec.security.gid},mode=0700`);
39941
+ }
39942
+ if (this.opts.network)
39943
+ args.push("--network", this.opts.network);
39944
+ if (this.opts.addHostGateway)
39945
+ args.push("--add-host", "host.docker.internal:host-gateway");
39946
+ for (const [key, value] of Object.entries(spec.env))
39947
+ args.push("-e", `${key}=${value}`);
39948
+ args.push(await resolveDockerImage(this.opts.dockerBin, spec.image), ...spec.command);
39949
+ try {
39950
+ const id = await run(this.opts.dockerBin, args);
39951
+ return { kind: this.kind, id, name: name2, poolRuntimeId: launch.runtimeId };
39952
+ } catch (error51) {
39953
+ await rm(inputFile, { force: true });
39954
+ throw error51;
39955
+ }
39956
+ }
39151
39957
  async inspect(ref) {
39152
39958
  try {
39153
39959
  const out = await run(this.opts.dockerBin, [
@@ -39176,6 +39982,11 @@ class DockerDriver {
39176
39982
  await run(this.opts.dockerBin, ["rm", "-f", ref.id]);
39177
39983
  } catch {}
39178
39984
  const name2 = typeof ref.name === "string" ? ref.name : "";
39985
+ const poolRuntimeId = typeof ref.poolRuntimeId === "string" ? ref.poolRuntimeId : "";
39986
+ if (poolRuntimeId) {
39987
+ await rm(this.inputPath(`pool-${poolRuntimeId}`), { force: true });
39988
+ return;
39989
+ }
39179
39990
  const workspaceId = name2.replace(/^pocketcoder-ws-/, "");
39180
39991
  if (workspaceId) {
39181
39992
  await rm(this.inputPath(workspaceId), { force: true });
@@ -39184,6 +39995,9 @@ class DockerDriver {
39184
39995
  async cleanupInput(workspaceId) {
39185
39996
  await rm(this.inputPath(workspaceId), { force: true });
39186
39997
  }
39998
+ async cleanupWarmInput(runtimeId) {
39999
+ await rm(this.inputPath(`pool-${runtimeId}`), { force: true });
40000
+ }
39187
40001
  async list() {
39188
40002
  const out = await run(this.opts.dockerBin, [
39189
40003
  "ps",
@@ -39205,6 +40019,32 @@ class DockerDriver {
39205
40019
  };
39206
40020
  });
39207
40021
  }
40022
+ async listWarm() {
40023
+ const out = await run(this.opts.dockerBin, [
40024
+ "ps",
40025
+ "--all",
40026
+ "--filter",
40027
+ `label=${POOL_RUNTIME_LABEL}`,
40028
+ "--format",
40029
+ `{{.ID}} {{.Label "${POOL_RUNTIME_LABEL}"}} {{.Label "${DIGEST_LABEL}"}}`
40030
+ ]);
40031
+ if (!out)
40032
+ return [];
40033
+ return out.split(`
40034
+ `).map((line4) => {
40035
+ const [id = "", runtimeId = "", templateDigest = ""] = line4.split("\t");
40036
+ return {
40037
+ runtimeId,
40038
+ templateDigest,
40039
+ ref: {
40040
+ kind: this.kind,
40041
+ id,
40042
+ name: `pocketcoder-pool-${runtimeId}`,
40043
+ poolRuntimeId: runtimeId
40044
+ }
40045
+ };
40046
+ });
40047
+ }
39208
40048
  }
39209
40049
  // ../drivers/src/file-secrets.ts
39210
40050
  import { lstat, realpath } from "fs/promises";
@@ -39761,6 +40601,7 @@ class FilesystemStorageDriver {
39761
40601
  // ../drivers/src/kubernetes.ts
39762
40602
  var KUBERNETES_WORKSPACE_LABEL = "pocketcoder.workspace";
39763
40603
  var KUBERNETES_DIGEST_ANNOTATION = "pocketcoder.dev/template-digest";
40604
+ var KUBERNETES_POOL_LABEL = "pocketcoder.pool-runtime";
39764
40605
  async function kubectl(bin, namespace, args, input) {
39765
40606
  const proc = Bun.spawn([bin, "--namespace", namespace, "--request-timeout=30s", ...args], {
39766
40607
  stdout: "pipe",
@@ -39989,6 +40830,102 @@ class KubernetesDriver {
39989
40830
  throw error51;
39990
40831
  }
39991
40832
  }
40833
+ async createWarm(launch) {
40834
+ const spec = launch.template.spec;
40835
+ const name2 = `pocketcoder-pool-${launch.runtimeId}`;
40836
+ const inputSecret = `${name2}-input`;
40837
+ await kubectl(this.kubectlBin, this.namespace, ["apply", "-f", "-"], JSON.stringify({
40838
+ apiVersion: "v1",
40839
+ kind: "Secret",
40840
+ metadata: { name: inputSecret, labels: { [KUBERNETES_POOL_LABEL]: launch.runtimeId } },
40841
+ type: "Opaque",
40842
+ stringData: { "input.json": JSON.stringify(launch.input) }
40843
+ }));
40844
+ const memory = spec.security.writableMemoryPaths.map((path, index2) => ({
40845
+ volume: { name: `memory-${index2}`, emptyDir: { medium: "Memory", sizeLimit: "256Mi" } },
40846
+ mount: { name: `memory-${index2}`, mountPath: path }
40847
+ }));
40848
+ const labels = { [KUBERNETES_POOL_LABEL]: launch.runtimeId };
40849
+ const annotations = { [KUBERNETES_DIGEST_ANNOTATION]: launch.template.digest };
40850
+ const manifest = {
40851
+ apiVersion: "batch/v1",
40852
+ kind: "Job",
40853
+ metadata: { name: name2, labels, annotations },
40854
+ spec: {
40855
+ backoffLimit: 0,
40856
+ ttlSecondsAfterFinished: 3600,
40857
+ template: {
40858
+ metadata: { labels, annotations },
40859
+ spec: {
40860
+ restartPolicy: "Never",
40861
+ automountServiceAccountToken: false,
40862
+ ...this.serviceAccountName ? { serviceAccountName: this.serviceAccountName } : {},
40863
+ securityContext: {
40864
+ runAsUser: spec.security.uid,
40865
+ runAsGroup: spec.security.gid,
40866
+ runAsNonRoot: true,
40867
+ fsGroup: spec.security.gid,
40868
+ fsGroupChangePolicy: "OnRootMismatch",
40869
+ seccompProfile: { type: spec.security.seccomp }
40870
+ },
40871
+ containers: [
40872
+ {
40873
+ name: "workspace",
40874
+ image: spec.image,
40875
+ imagePullPolicy: this.imagePullPolicy,
40876
+ command: spec.command,
40877
+ env: Object.entries(spec.env).map(([name3, value]) => ({ name: name3, value })),
40878
+ resources: { requests: spec.resources, limits: spec.resources },
40879
+ securityContext: {
40880
+ readOnlyRootFilesystem: spec.security.readOnlyRoot,
40881
+ allowPrivilegeEscalation: spec.security.allowPrivilegeEscalation,
40882
+ capabilities: { drop: spec.security.dropCapabilities }
40883
+ },
40884
+ volumeMounts: [
40885
+ {
40886
+ name: "provider-input",
40887
+ mountPath: "/run/pocketcoder/input",
40888
+ subPath: "input.json",
40889
+ readOnly: true
40890
+ },
40891
+ ...memory.map((item) => item.mount)
40892
+ ]
40893
+ }
40894
+ ],
40895
+ volumes: [
40896
+ {
40897
+ name: "provider-input",
40898
+ secret: {
40899
+ secretName: inputSecret,
40900
+ items: [{ key: "input.json", path: "input.json" }]
40901
+ }
40902
+ },
40903
+ ...memory.map((item) => item.volume)
40904
+ ]
40905
+ }
40906
+ }
40907
+ }
40908
+ };
40909
+ try {
40910
+ await kubectl(this.kubectlBin, this.namespace, ["apply", "-f", "-"], JSON.stringify(manifest));
40911
+ return {
40912
+ kind: this.kind,
40913
+ id: name2,
40914
+ name: name2,
40915
+ inputSecret,
40916
+ namespace: this.namespace,
40917
+ poolRuntimeId: launch.runtimeId
40918
+ };
40919
+ } catch (error51) {
40920
+ await kubectl(this.kubectlBin, this.namespace, [
40921
+ "delete",
40922
+ "secret",
40923
+ inputSecret,
40924
+ "--ignore-not-found"
40925
+ ]).catch(() => {});
40926
+ throw error51;
40927
+ }
40928
+ }
39992
40929
  async inspect(ref) {
39993
40930
  try {
39994
40931
  const output = await kubectl(this.kubectlBin, this.namespace, [
@@ -40022,7 +40959,7 @@ class KubernetesDriver {
40022
40959
  "delete",
40023
40960
  "pod",
40024
40961
  "-l",
40025
- `${KUBERNETES_WORKSPACE_LABEL}=${String(ref.name ?? ref.id).replace(/^pocketcoder-ws-/, "")}`,
40962
+ `job-name=${ref.id}`,
40026
40963
  `--grace-period=${graceSeconds}`,
40027
40964
  "--wait=true",
40028
40965
  "--ignore-not-found"
@@ -40052,6 +40989,14 @@ class KubernetesDriver {
40052
40989
  "--ignore-not-found"
40053
40990
  ]).catch(() => {});
40054
40991
  }
40992
+ async cleanupWarmInput(runtimeId) {
40993
+ await kubectl(this.kubectlBin, this.namespace, [
40994
+ "delete",
40995
+ "secret",
40996
+ `pocketcoder-pool-${runtimeId}-input`,
40997
+ "--ignore-not-found"
40998
+ ]).catch(() => {});
40999
+ }
40055
41000
  async list() {
40056
41001
  const output = await kubectl(this.kubectlBin, this.namespace, [
40057
41002
  "get",
@@ -40083,6 +41028,38 @@ class KubernetesDriver {
40083
41028
  ];
40084
41029
  });
40085
41030
  }
41031
+ async listWarm() {
41032
+ const output = await kubectl(this.kubectlBin, this.namespace, [
41033
+ "get",
41034
+ "jobs",
41035
+ "-l",
41036
+ KUBERNETES_POOL_LABEL,
41037
+ "-o",
41038
+ "json"
41039
+ ]);
41040
+ const list = JSON.parse(output);
41041
+ return (list.items ?? []).flatMap((job) => {
41042
+ const runtimeId = job.metadata?.labels?.[KUBERNETES_POOL_LABEL];
41043
+ const name2 = job.metadata?.name;
41044
+ const templateDigest = job.metadata?.annotations?.[KUBERNETES_DIGEST_ANNOTATION];
41045
+ if (!runtimeId || !name2 || !templateDigest)
41046
+ return [];
41047
+ return [
41048
+ {
41049
+ runtimeId,
41050
+ templateDigest,
41051
+ ref: {
41052
+ kind: this.kind,
41053
+ id: name2,
41054
+ name: name2,
41055
+ inputSecret: `${name2}-input`,
41056
+ namespace: this.namespace,
41057
+ poolRuntimeId: runtimeId
41058
+ }
41059
+ }
41060
+ ];
41061
+ });
41062
+ }
40086
41063
  }
40087
41064
  // ../drivers/src/kubernetes-secrets.ts
40088
41065
  function collect(workspace2) {
@@ -40189,13 +41166,18 @@ class KubernetesPvcStorageDriver {
40189
41166
  class FakeDriver {
40190
41167
  kind = "fake";
40191
41168
  created = [];
41169
+ warmCreated = [];
40192
41170
  terminated = [];
40193
41171
  stopped = [];
40194
41172
  failNextCreate = false;
41173
+ createDelayMs = 0;
40195
41174
  counter = 0;
40196
41175
  live = new Map;
40197
41176
  stoppedIds = new Set;
41177
+ warm = new Map;
40198
41178
  async create(launch) {
41179
+ if (this.createDelayMs > 0)
41180
+ await new Promise((resolve7) => setTimeout(resolve7, this.createDelayMs));
40199
41181
  if (this.failNextCreate) {
40200
41182
  this.failNextCreate = false;
40201
41183
  throw new Error("fake driver create failure");
@@ -40210,8 +41192,23 @@ class FakeDriver {
40210
41192
  });
40211
41193
  return ref;
40212
41194
  }
41195
+ async createWarm(launch) {
41196
+ this.warmCreated.push(launch);
41197
+ this.counter += 1;
41198
+ const ref = {
41199
+ kind: this.kind,
41200
+ id: `fake-warm-${this.counter}`,
41201
+ poolRuntimeId: launch.runtimeId
41202
+ };
41203
+ this.warm.set(launch.runtimeId, {
41204
+ runtimeId: launch.runtimeId,
41205
+ templateDigest: launch.template.digest,
41206
+ ref
41207
+ });
41208
+ return ref;
41209
+ }
40213
41210
  async inspect(ref) {
40214
- const exists3 = [...this.live.values()].some((p) => p.ref.id === ref.id);
41211
+ const exists3 = [...this.live.values(), ...this.warm.values()].some((p) => p.ref.id === ref.id);
40215
41212
  return {
40216
41213
  exists: exists3,
40217
41214
  running: exists3 && !this.stoppedIds.has(ref.id),
@@ -40229,10 +41226,16 @@ class FakeDriver {
40229
41226
  if (p.ref.id === ref.id)
40230
41227
  this.live.delete(wsId);
40231
41228
  }
41229
+ for (const [id, p] of this.warm)
41230
+ if (p.ref.id === ref.id)
41231
+ this.warm.delete(id);
40232
41232
  }
40233
41233
  async list() {
40234
41234
  return [...this.live.values()];
40235
41235
  }
41236
+ async listWarm() {
41237
+ return [...this.warm.values()];
41238
+ }
40236
41239
  inputFor(workspaceId) {
40237
41240
  return this.created.find((l) => l.workspace.id === workspaceId)?.input;
40238
41241
  }
@@ -40241,7 +41244,7 @@ class FakeDriver {
40241
41244
  var PLACEHOLDER_DIGEST = "a".repeat(64);
40242
41245
  var OTHER_DIGEST = "b".repeat(64);
40243
41246
  // ../testkit/src/memory-store.ts
40244
- import { randomUUID as randomUUID7 } from "crypto";
41247
+ import { randomUUID as randomUUID8 } from "crypto";
40245
41248
  var ACTIVE_STATES = [
40246
41249
  "provisioning",
40247
41250
  "connected",
@@ -40250,6 +41253,8 @@ var ACTIVE_STATES = [
40250
41253
  "terminating"
40251
41254
  ];
40252
41255
  var MAX_LOG_BYTES2 = 10 * 1024 * 1024;
41256
+ var MAX_CONVERSATION_BYTES2 = 50 * 1024 * 1024;
41257
+ var MAX_CONVERSATION_MESSAGES2 = 1e5;
40253
41258
  var CHANGE_PATCH_KEYS = new Set([
40254
41259
  "connectedAt",
40255
41260
  "disconnectedAt",
@@ -40259,6 +41264,7 @@ var CHANGE_PATCH_KEYS = new Set([
40259
41264
  "failureLogTailTruncated",
40260
41265
  "failureLastLogSeq",
40261
41266
  "resolvedSource",
41267
+ "persistenceCapability",
40262
41268
  "latestCheckpointId",
40263
41269
  "outputs"
40264
41270
  ]);
@@ -40268,6 +41274,7 @@ class MemoryStore {
40268
41274
  principals = [];
40269
41275
  keys = [];
40270
41276
  workspaces = new Map;
41277
+ warmPoolRuntimes = new Map;
40271
41278
  history = [];
40272
41279
  outbox = [];
40273
41280
  logs = new Map;
@@ -40277,6 +41284,9 @@ class MemoryStore {
40277
41284
  checkpoints = new Map;
40278
41285
  operations = new Map;
40279
41286
  outputs = new Map;
41287
+ conversations = new Map;
41288
+ conversationBytes = new Map;
41289
+ conversationStates = new Map;
40280
41290
  changeWaiters = new Map;
40281
41291
  async init() {}
40282
41292
  async close() {
@@ -40303,7 +41313,7 @@ class MemoryStore {
40303
41313
  return { row: existing, created: false, conflict: false };
40304
41314
  }
40305
41315
  const row = {
40306
- id: randomUUID7(),
41316
+ id: randomUUID8(),
40307
41317
  name: input.name,
40308
41318
  version: input.version,
40309
41319
  digest: input.digest,
@@ -40338,12 +41348,58 @@ class MemoryStore {
40338
41348
  row.retiredAt = status === "retired" ? new Date : null;
40339
41349
  }
40340
41350
  }
41351
+ async insertWarmPoolRuntime(row) {
41352
+ const existing = this.warmPoolRuntimes.get(row.id);
41353
+ if (existing)
41354
+ return { ...existing };
41355
+ this.warmPoolRuntimes.set(row.id, { ...row });
41356
+ return { ...row };
41357
+ }
41358
+ async getWarmPoolRuntime(id) {
41359
+ const row = this.warmPoolRuntimes.get(id);
41360
+ return row ? { ...row } : null;
41361
+ }
41362
+ async listWarmPoolRuntimes() {
41363
+ return [...this.warmPoolRuntimes.values()].map((row) => ({ ...row }));
41364
+ }
41365
+ async updateWarmPoolRuntime(id, patch, at) {
41366
+ const row = this.warmPoolRuntimes.get(id);
41367
+ if (!row)
41368
+ return;
41369
+ Object.assign(row, patch);
41370
+ row.updatedAt = at;
41371
+ }
41372
+ async claimWarmPoolRuntime(claim) {
41373
+ const workspace2 = this.workspaces.get(claim.workspaceId);
41374
+ if (workspace2?.state !== "queued")
41375
+ return null;
41376
+ const runtime = [...this.warmPoolRuntimes.values()].filter((row) => row.state === "ready" && row.templateDigest === claim.templateDigest && row.driverKind === claim.driverKind && row.eligibilityFingerprint === claim.eligibilityFingerprint).sort((a, b) => (a.readyAt?.getTime() ?? 0) - (b.readyAt?.getTime() ?? 0))[0];
41377
+ if (!runtime?.providerRef)
41378
+ return null;
41379
+ runtime.state = "leasing";
41380
+ runtime.workspaceId = workspace2.id;
41381
+ runtime.leasedAt = claim.at;
41382
+ runtime.updatedAt = claim.at;
41383
+ workspace2.state = "provisioning";
41384
+ workspace2.provisioningMode = "warm";
41385
+ workspace2.providerKind = runtime.driverKind;
41386
+ workspace2.providerRef = runtime.providerRef;
41387
+ workspace2.registrationDigest = claim.registrationDigest;
41388
+ workspace2.registrationExpiresAt = claim.registrationExpiresAt;
41389
+ workspace2.launchAttempts += 1;
41390
+ workspace2.changeSeq += 1;
41391
+ workspace2.updatedAt = claim.at;
41392
+ this.appendHistory(workspace2, "queued", "provisioning", null, claim.at);
41393
+ this.appendWorkspaceEvent(workspace2, claim.at);
41394
+ this.notifyWorkspaceChange(workspace2.id);
41395
+ return { runtime: { ...runtime }, workspace: { ...workspace2 } };
41396
+ }
40341
41397
  async createPrincipal(name2, scopes2, templateNames) {
40342
41398
  if (this.principals.some((p) => p.name === name2)) {
40343
41399
  throw new Error(`principal exists: ${name2}`);
40344
41400
  }
40345
41401
  const row = {
40346
- id: randomUUID7(),
41402
+ id: randomUUID8(),
40347
41403
  name: name2,
40348
41404
  scopes: scopes2,
40349
41405
  templateNames,
@@ -40426,6 +41482,7 @@ class MemoryStore {
40426
41482
  launchInput: row.launchInput,
40427
41483
  providerKind: null,
40428
41484
  providerRef: null,
41485
+ provisioningMode: null,
40429
41486
  registrationDigest: null,
40430
41487
  registrationExpiresAt: null,
40431
41488
  reconnectDigest: null,
@@ -40467,7 +41524,15 @@ class MemoryStore {
40467
41524
  rows = rows.filter((w) => w.state === filter.state);
40468
41525
  if (filter.template)
40469
41526
  rows = rows.filter((w) => w.templateName === filter.template);
40470
- rows.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
41527
+ if (filter.metadata) {
41528
+ rows = rows.filter((w) => Object.entries(filter.metadata ?? {}).every(([key, value]) => w.metadata[key] === value));
41529
+ }
41530
+ const { createdAfter, createdBefore } = filter;
41531
+ if (createdAfter)
41532
+ rows = rows.filter((w) => w.createdAt >= createdAfter);
41533
+ if (createdBefore)
41534
+ rows = rows.filter((w) => w.createdAt < createdBefore);
41535
+ rows.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || b.id.localeCompare(a.id));
40471
41536
  if (filter.cursor) {
40472
41537
  const idx = rows.findIndex((w) => w.id === filter.cursor);
40473
41538
  if (idx >= 0)
@@ -40561,6 +41626,16 @@ class MemoryStore {
40561
41626
  row.terminalAt = req.at;
40562
41627
  row.launchInput = null;
40563
41628
  row.registrationDigest = null;
41629
+ const current = this.conversationStates.get(id);
41630
+ if (current?.status !== "deleted") {
41631
+ this.conversationStates.set(id, {
41632
+ workspaceId: id,
41633
+ status: "retained",
41634
+ expiresAt: new Date(req.at.getTime() + parseDurationMs(row.templateSnapshot.spec.persistence.conversationRetention)),
41635
+ deletedAt: null,
41636
+ updatedAt: req.at
41637
+ });
41638
+ }
40564
41639
  }
40565
41640
  this.appendHistory(row, fromState, req.to, row.reasonCode, req.at);
40566
41641
  this.appendWorkspaceEvent(row, req.at);
@@ -40677,7 +41752,7 @@ class MemoryStore {
40677
41752
  }
40678
41753
  appendHistory(row, from, to, reason, at) {
40679
41754
  this.history.push({
40680
- id: randomUUID7(),
41755
+ id: randomUUID8(),
40681
41756
  workspaceId: row.id,
40682
41757
  fromState: from,
40683
41758
  toState: to,
@@ -40735,6 +41810,76 @@ class MemoryStore {
40735
41810
  lastSeq
40736
41811
  };
40737
41812
  }
41813
+ async appendConversationMessage(input) {
41814
+ const state = this.conversationStates.get(input.workspaceId);
41815
+ if (state?.status === "deleted")
41816
+ throw new Error("conversation.deleted");
41817
+ const rows = this.conversations.get(input.workspaceId) ?? [];
41818
+ const existing = rows.find((row) => row.messageId === input.messageId);
41819
+ if (existing)
41820
+ return { message: { ...existing }, created: false };
41821
+ const storedBytes = this.conversationBytes.get(input.workspaceId) ?? 0;
41822
+ const inputBytes = Buffer.byteLength(input.content) + Buffer.byteLength(JSON.stringify(input.metadata));
41823
+ if (rows.length >= MAX_CONVERSATION_MESSAGES2 || storedBytes + inputBytes > MAX_CONVERSATION_BYTES2) {
41824
+ throw new Error("conversation.quota_exceeded");
41825
+ }
41826
+ const message = { ...input, seq: (rows.at(-1)?.seq ?? 0) + 1 };
41827
+ rows.push(message);
41828
+ this.conversations.set(input.workspaceId, rows);
41829
+ this.conversationBytes.set(input.workspaceId, storedBytes + inputBytes);
41830
+ if (!state) {
41831
+ this.conversationStates.set(input.workspaceId, {
41832
+ workspaceId: input.workspaceId,
41833
+ status: "retained",
41834
+ expiresAt: null,
41835
+ deletedAt: null,
41836
+ updatedAt: input.createdAt
41837
+ });
41838
+ }
41839
+ return { message: { ...message }, created: true };
41840
+ }
41841
+ async readConversation(workspaceId, afterSeq, limit) {
41842
+ return (this.conversations.get(workspaceId) ?? []).filter((row) => row.seq > afterSeq).slice(0, limit).map((row) => ({ ...row, metadata: { ...row.metadata } }));
41843
+ }
41844
+ async getConversationState(workspaceId) {
41845
+ const row = this.conversationStates.get(workspaceId);
41846
+ return row ? { ...row } : null;
41847
+ }
41848
+ async setConversationExpiry(workspaceId, expiresAt, at) {
41849
+ const current = this.conversationStates.get(workspaceId);
41850
+ if (current?.status === "deleted")
41851
+ return;
41852
+ this.conversationStates.set(workspaceId, {
41853
+ workspaceId,
41854
+ status: "retained",
41855
+ expiresAt,
41856
+ deletedAt: null,
41857
+ updatedAt: at
41858
+ });
41859
+ }
41860
+ async deleteConversation(workspaceId, at) {
41861
+ this.conversations.delete(workspaceId);
41862
+ this.conversationBytes.delete(workspaceId);
41863
+ this.conversationStates.set(workspaceId, {
41864
+ workspaceId,
41865
+ status: "deleted",
41866
+ expiresAt: null,
41867
+ deletedAt: at,
41868
+ updatedAt: at
41869
+ });
41870
+ }
41871
+ async pruneExpiredConversations(at) {
41872
+ let deleted = 0;
41873
+ for (const state of this.conversationStates.values()) {
41874
+ if (state.status !== "retained" || !state.expiresAt || state.expiresAt > at)
41875
+ continue;
41876
+ if ((this.conversations.get(state.workspaceId)?.length ?? 0) > 0)
41877
+ deleted += 1;
41878
+ this.conversations.delete(state.workspaceId);
41879
+ this.conversationBytes.delete(state.workspaceId);
41880
+ }
41881
+ return deleted;
41882
+ }
40738
41883
  async claimDueEvents(now, limit) {
40739
41884
  const due = this.outbox.filter((e) => !e.deliveredAt && e.nextAttemptAt <= now && !this.claimedEvents.has(e.id)).sort((a, b) => a.occurredAt.getTime() - b.occurredAt.getTime()).slice(0, limit);
40740
41885
  for (const e of due)
@@ -40760,7 +41905,7 @@ class MemoryStore {
40760
41905
  }
40761
41906
  async appendEvent(workspaceId, eventType, payload, at) {
40762
41907
  this.outbox.push({
40763
- id: randomUUID7(),
41908
+ id: randomUUID8(),
40764
41909
  workspaceId,
40765
41910
  eventType,
40766
41911
  payload,
@@ -40772,6 +41917,9 @@ class MemoryStore {
40772
41917
  });
40773
41918
  }
40774
41919
  }
41920
+ // ../../apps/pocketcoder-server/src/app.ts
41921
+ import { randomUUID as randomUUID11 } from "crypto";
41922
+
40775
41923
  // ../../node_modules/.bun/@asteasolutions+zod-to-openapi@8.5.0+68a1e3a0c4588df3/node_modules/@asteasolutions/zod-to-openapi/dist/index.mjs
40776
41924
  function __rest(s, e) {
40777
41925
  var t = {};
@@ -44393,7 +45541,7 @@ var createBunWebSocket = () => ({
44393
45541
  });
44394
45542
 
44395
45543
  // ../../apps/pocketcoder-server/src/hub.ts
44396
- import { randomUUID as randomUUID8 } from "crypto";
45544
+ import { randomUUID as randomUUID9 } from "crypto";
44397
45545
  var MAX_INFLIGHT_RELAY = 16;
44398
45546
 
44399
45547
  class Hub {
@@ -44502,12 +45650,12 @@ class Hub {
44502
45650
  relay(workspaceId, request) {
44503
45651
  const conn = this.byWorkspace.get(workspaceId);
44504
45652
  if (!conn?.registered) {
44505
- return Promise.resolve({ request_id: randomUUID8(), headers: {}, error_code: "unreachable" });
45653
+ return Promise.resolve({ request_id: randomUUID9(), headers: {}, error_code: "unreachable" });
44506
45654
  }
44507
45655
  if (conn.inflight.size >= MAX_INFLIGHT_RELAY) {
44508
- return Promise.resolve({ request_id: randomUUID8(), headers: {}, error_code: "deadline" });
45656
+ return Promise.resolve({ request_id: randomUUID9(), headers: {}, error_code: "deadline" });
44509
45657
  }
44510
- const requestId = randomUUID8();
45658
+ const requestId = randomUUID9();
44511
45659
  return new Promise((resolve7) => {
44512
45660
  const timer = setTimeout(() => {
44513
45661
  conn.inflight.delete(requestId);
@@ -44543,9 +45691,9 @@ class Hub {
44543
45691
  }
44544
45692
 
44545
45693
  // ../../apps/pocketcoder-server/src/middleware.ts
44546
- import { randomUUID as randomUUID9 } from "crypto";
45694
+ import { randomUUID as randomUUID10 } from "crypto";
44547
45695
  var requestId = async (c, next) => {
44548
- c.set("requestId", randomUUID9());
45696
+ c.set("requestId", randomUUID10());
44549
45697
  await next();
44550
45698
  };
44551
45699
  function machineAuth(store2, pepper) {
@@ -44583,14 +45731,100 @@ function requireScope(scope) {
44583
45731
  };
44584
45732
  }
44585
45733
  function handleError(err, c) {
44586
- const id = c.get("requestId") ?? randomUUID9();
45734
+ const id = c.get("requestId") ?? randomUUID10();
44587
45735
  if (err instanceof ApiError) {
44588
- return c.json(errorEnvelope(err.code, err.message, id), err.status);
45736
+ return c.json(errorEnvelope(err.code, err.message, id, err.details), err.status);
44589
45737
  }
44590
45738
  console.error(`[pocketcoder] request ${id} failed:`, err instanceof Error ? err.message : err);
44591
45739
  return c.json(errorEnvelope("internal.error", "Internal error.", id), 500);
44592
45740
  }
44593
45741
 
45742
+ // ../../apps/pocketcoder-server/src/pool-ws.ts
45743
+ class PoolConnectionHub {
45744
+ connections = new Map;
45745
+ attach(runtimeId, ws) {
45746
+ this.connections.get(runtimeId)?.close(1008, "superseded pool connection");
45747
+ this.connections.set(runtimeId, ws);
45748
+ }
45749
+ detach(runtimeId, ws) {
45750
+ if (this.connections.get(runtimeId) === ws)
45751
+ this.connections.delete(runtimeId);
45752
+ }
45753
+ assign(runtimeId, input) {
45754
+ const ws = this.connections.get(runtimeId);
45755
+ if (!ws)
45756
+ return false;
45757
+ const frame = LeaseAssignmentFrameSchema.parse({
45758
+ v: POOL_PROTOCOL_VERSION,
45759
+ type: "lease_assignment",
45760
+ input
45761
+ });
45762
+ try {
45763
+ ws.send(JSON.stringify(frame));
45764
+ return true;
45765
+ } catch {
45766
+ return false;
45767
+ }
45768
+ }
45769
+ isConnected(runtimeId) {
45770
+ return this.connections.has(runtimeId);
45771
+ }
45772
+ close(runtimeId) {
45773
+ const ws = this.connections.get(runtimeId);
45774
+ this.connections.delete(runtimeId);
45775
+ ws?.close(1000, "pool runtime drained");
45776
+ }
45777
+ }
45778
+ function poolConnectValidator(deps) {
45779
+ return async (c, next) => {
45780
+ const reject = (message) => c.json(errorEnvelope("auth.invalid_key", message, c.get("requestId") ?? ""), 401);
45781
+ if (Number(c.req.header(HEADER_PROTOCOL)) !== POOL_PROTOCOL_VERSION)
45782
+ return reject("Unsupported pool protocol version.");
45783
+ const runtimeId = c.req.header(HEADER_POOL_RUNTIME) ?? "";
45784
+ const secret = c.req.header(HEADER_POOL_ENROLLMENT) ?? "";
45785
+ const row = runtimeId ? await deps.store.getWarmPoolRuntime(runtimeId) : null;
45786
+ if (row?.state !== "provisioning" || !row.enrollmentDigest || !row.enrollmentExpiresAt || row.enrollmentExpiresAt <= new Date || !verifyOpaque(deps.pepper, secret, row.enrollmentDigest)) {
45787
+ return reject("Invalid or expired pool enrollment credential.");
45788
+ }
45789
+ c.set("poolAuth", { runtimeId });
45790
+ await next();
45791
+ };
45792
+ }
45793
+ function poolWsEvents(deps) {
45794
+ return (c) => {
45795
+ const auth = c.get("poolAuth");
45796
+ let registered = false;
45797
+ return {
45798
+ onMessage: (event, ws) => {
45799
+ (async () => {
45800
+ if (registered) {
45801
+ ws.close(1008, "pool runtime already registered");
45802
+ return;
45803
+ }
45804
+ const raw2 = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf8");
45805
+ const parsed = PoolRegisteredFrameSchema.safeParse(JSON.parse(raw2));
45806
+ const row = await deps.store.getWarmPoolRuntime(auth.runtimeId);
45807
+ if (!parsed.success || !row || parsed.data.pool_runtime_id !== row.id || parsed.data.template.digest !== row.templateDigest || parsed.data.template.name !== row.templateName || parsed.data.template.version !== row.templateVersion) {
45808
+ ws.close(1008, "invalid pool registration");
45809
+ return;
45810
+ }
45811
+ deps.hub.attach(row.id, ws);
45812
+ registered = await deps.manager.markReady(row.id);
45813
+ if (!registered)
45814
+ ws.close(1008, "pool registration no longer valid");
45815
+ })().catch((error51) => {
45816
+ deps.log?.(`pool ws ${auth.runtimeId}: ${String(error51)}`);
45817
+ ws.close(1011, "pool registration failed");
45818
+ });
45819
+ },
45820
+ onClose: (_event, ws) => {
45821
+ deps.hub.detach(auth.runtimeId, ws);
45822
+ deps.manager.disconnected(auth.runtimeId);
45823
+ }
45824
+ };
45825
+ };
45826
+ }
45827
+
44594
45828
  // ../../apps/pocketcoder-server/src/relay.ts
44595
45829
  var SAFE_RESPONSE_HEADERS = ["content-type"];
44596
45830
  function requestPath(c, workspaceId, serviceName) {
@@ -44933,6 +46167,25 @@ async function handleConnectedFrame(deps, connection, ws, frame, closeProtocol)
44933
46167
  return;
44934
46168
  case "restore_status":
44935
46169
  return;
46170
+ case "conversation_message": {
46171
+ const workspace2 = await deps.store.getWorkspace(frame.workspace_id);
46172
+ if (!workspace2 || isTerminal(workspace2.state))
46173
+ return;
46174
+ try {
46175
+ await deps.store.appendConversationMessage({
46176
+ workspaceId: frame.workspace_id,
46177
+ messageId: frame.payload.message_id,
46178
+ role: frame.payload.role,
46179
+ content: frame.payload.content,
46180
+ occurredAt: new Date(frame.payload.occurred_at),
46181
+ metadata: frame.payload.metadata,
46182
+ createdAt: new Date
46183
+ });
46184
+ } catch (error51) {
46185
+ deps.log?.(`conversation ${frame.workspace_id}: ${String(error51)}`);
46186
+ }
46187
+ return;
46188
+ }
44936
46189
  }
44937
46190
  }
44938
46191
  function parseFrame(data, auth, connection, ws, closeProtocol) {
@@ -45026,6 +46279,35 @@ function safeTemplateItem(row) {
45026
46279
  status: row.status
45027
46280
  };
45028
46281
  }
46282
+ var WorkspaceMetadataFilterSchema = exports_external.record(exports_external.string().min(1).max(64), exports_external.string().max(512)).refine((value) => Object.keys(value).length <= 16, "metadata filter has at most 16 keys");
46283
+ function parseMetadataFilter(value) {
46284
+ if (!value)
46285
+ return;
46286
+ try {
46287
+ const parsed = WorkspaceMetadataFilterSchema.safeParse(JSON.parse(value));
46288
+ if (parsed.success)
46289
+ return parsed.data;
46290
+ } catch {}
46291
+ throw new ApiError("validation.invalid", "metadata must be a JSON object containing bounded string keys and values.");
46292
+ }
46293
+ function workspaceListFilterOf(query) {
46294
+ const metadata = parseMetadataFilter(query.metadata);
46295
+ const createdAfter = query.created_after ? new Date(query.created_after) : undefined;
46296
+ const createdBefore = query.created_before ? new Date(query.created_before) : undefined;
46297
+ if (createdAfter && createdBefore && createdAfter >= createdBefore) {
46298
+ throw new ApiError("validation.invalid", "created_after must be before created_before.");
46299
+ }
46300
+ return {
46301
+ ...query.external_id ? { externalId: query.external_id } : {},
46302
+ ...query.state ? { state: query.state } : {},
46303
+ ...query.template ? { template: query.template } : {},
46304
+ ...metadata ? { metadata } : {},
46305
+ ...createdAfter ? { createdAfter } : {},
46306
+ ...createdBefore ? { createdBefore } : {},
46307
+ limit: query.limit,
46308
+ ...query.cursor ? { cursor: query.cursor } : {}
46309
+ };
46310
+ }
45029
46311
  var WorkspaceCreateHeadersSchema = exports_external.object({
45030
46312
  "Idempotency-Key": exports_external.string().min(1).max(256).openapi({
45031
46313
  description: "Opaque caller key. Reusing it with the same request returns the original workspace; a different request returns idempotency.conflict.",
@@ -45035,6 +46317,20 @@ var WorkspaceCreateHeadersSchema = exports_external.object({
45035
46317
  function buildServer(deps) {
45036
46318
  const { store: store2, driver: driver2, pepper, limits, log } = deps;
45037
46319
  const hub = new Hub;
46320
+ const poolHub = new PoolConnectionHub;
46321
+ const secretFactory = {
46322
+ generate: generateOpaqueSecret,
46323
+ digest: (secret) => digestOpaque(pepper, secret)
46324
+ };
46325
+ const warmPool = deps.warmPools ? new WarmPoolManager({
46326
+ store: store2,
46327
+ driver: driver2,
46328
+ connections: poolHub,
46329
+ secrets: secretFactory,
46330
+ workspaceServerUrl: deps.workspaceServerUrl,
46331
+ pools: deps.warmPools,
46332
+ onError: (context, error51) => log?.(`${context}: ${String(error51)}`)
46333
+ }) : undefined;
45038
46334
  const persistenceHolder = {};
45039
46335
  const scheduler2 = new Scheduler({
45040
46336
  store: store2,
@@ -45042,12 +46338,10 @@ function buildServer(deps) {
45042
46338
  ...deps.storageDriver ? { storageDriver: deps.storageDriver } : {},
45043
46339
  ...deps.secretResolver ? { secretResolver: deps.secretResolver } : {},
45044
46340
  connections: hub,
45045
- secrets: {
45046
- generate: generateOpaqueSecret,
45047
- digest: (secret) => digestOpaque(pepper, secret)
45048
- },
46341
+ secrets: secretFactory,
45049
46342
  limits,
45050
46343
  workspaceServerUrl: deps.workspaceServerUrl,
46344
+ ...warmPool ? { warmPool } : {},
45051
46345
  preserveByPolicy: async (row, trigger) => persistenceHolder.service?.preserveByPolicy(row, trigger) ?? false,
45052
46346
  onError: (context, err) => log?.(`scheduler ${context}: ${String(err)}`)
45053
46347
  });
@@ -45091,6 +46385,9 @@ function buildServer(deps) {
45091
46385
  persistence: persistence2
45092
46386
  };
45093
46387
  app.get("/v1/agent/connect", agentConnectValidator(wsDeps), upgradeWebSocket2(agentWsEvents(wsDeps)));
46388
+ if (warmPool) {
46389
+ app.get("/v1/agent/pool-connect", poolConnectValidator({ store: store2, pepper }), upgradeWebSocket2(poolWsEvents({ store: store2, hub: poolHub, manager: warmPool, ...log ? { log } : {} })));
46390
+ }
45094
46391
  app.doc("/v1/openapi.json", {
45095
46392
  openapi: "3.0.0",
45096
46393
  info: {
@@ -45120,6 +46417,26 @@ function buildServer(deps) {
45120
46417
  const rows = await store2.listTemplates(names);
45121
46418
  return c.json({ items: rows.map(safeTemplateItem) }, 200);
45122
46419
  });
46420
+ if (warmPool) {
46421
+ app.openapi(createRoute({
46422
+ method: "get",
46423
+ path: "/v1/warm-pools",
46424
+ middleware: [requireScope("admin")],
46425
+ responses: {
46426
+ 200: {
46427
+ description: "Warm runtime inventory and cumulative metrics",
46428
+ content: {
46429
+ "application/json": {
46430
+ schema: exports_external.object({
46431
+ items: exports_external.array(exports_external.record(exports_external.string(), exports_external.unknown())),
46432
+ metrics: exports_external.record(exports_external.string(), exports_external.number())
46433
+ })
46434
+ }
46435
+ }
46436
+ }
46437
+ }
46438
+ }), async (c) => c.json(await warmPool.inventory(), 200));
46439
+ }
45123
46440
  app.openapi(createRoute({
45124
46441
  method: "get",
45125
46442
  path: "/v1/storage/inventory",
@@ -45152,13 +46469,21 @@ function buildServer(deps) {
45152
46469
  "application/json": {
45153
46470
  schema: exports_external.object({
45154
46471
  deleted: exports_external.number(),
45155
- skipped: exports_external.number()
46472
+ skipped: exports_external.number(),
46473
+ transcripts_deleted: exports_external.number()
45156
46474
  })
45157
46475
  }
45158
46476
  }
45159
46477
  }
45160
46478
  }
45161
- }), async (c) => c.json(await persistence2.pruneExpired(), 200));
46479
+ }), async (c) => {
46480
+ const result = await persistence2.pruneExpired();
46481
+ return c.json({
46482
+ deleted: result.deleted,
46483
+ skipped: result.skipped,
46484
+ transcripts_deleted: result.transcriptsDeleted
46485
+ }, 200);
46486
+ });
45162
46487
  app.openapi(createRoute({
45163
46488
  method: "post",
45164
46489
  path: "/v1/workspaces/{id}/preserve",
@@ -45366,6 +46691,131 @@ function buildServer(deps) {
45366
46691
  operation: toOperationResource(result.operation)
45367
46692
  }, 202);
45368
46693
  });
46694
+ app.openapi(createRoute({
46695
+ method: "post",
46696
+ path: "/v1/workspaces/{id}/resume",
46697
+ middleware: [requireScope("workspaces:restore")],
46698
+ request: {
46699
+ params: exports_external.object({ id: exports_external.uuid() }),
46700
+ headers: exports_external.object({ "idempotency-key": exports_external.string().min(1).max(256) }),
46701
+ body: { content: { "application/json": { schema: RestoreRequestSchema } } }
46702
+ },
46703
+ responses: {
46704
+ 202: {
46705
+ description: "New workspace queued with supported conversation context",
46706
+ content: {
46707
+ "application/json": {
46708
+ schema: exports_external.object({
46709
+ workspace: WorkspaceResourceSchema,
46710
+ operation: OperationResourceSchema,
46711
+ resume: ConversationResumeOutcomeSchema
46712
+ })
46713
+ }
46714
+ }
46715
+ }
46716
+ }
46717
+ }), async (c) => {
46718
+ const principal = c.get("principal");
46719
+ const source = await service.getOwned(principal, c.req.valid("param").id);
46720
+ const checkpoints = await store2.listCheckpoints(principal.id, {
46721
+ workspaceId: source.id,
46722
+ state: "ready"
46723
+ });
46724
+ const checkpoint = checkpoints[0];
46725
+ if (!checkpoint) {
46726
+ throw new ApiError("checkpoint.none_ready", "Workspace has no ready checkpoint.");
46727
+ }
46728
+ if (checkpoint.conversationRestore !== "supported") {
46729
+ const reason = checkpoint.conversationRestore === "filesystem_only" ? "filesystem_only" : "capability_unknown";
46730
+ throw new ApiError("resume.unsupported", "This checkpoint cannot restore conversation context.", { reason, checkpoint_id: checkpoint.id });
46731
+ }
46732
+ const result = await persistence2.restore(principal, checkpoint.id, c.req.valid("json"), c.req.valid("header")["idempotency-key"]);
46733
+ const workspace2 = await service.getOwned(principal, result.workspaceId);
46734
+ return c.json({
46735
+ workspace: toResource(workspace2),
46736
+ operation: toOperationResource(result.operation),
46737
+ resume: {
46738
+ status: "supported",
46739
+ reason: null,
46740
+ source_workspace_id: source.id,
46741
+ checkpoint_id: checkpoint.id
46742
+ }
46743
+ }, 202);
46744
+ });
46745
+ app.openapi(createRoute({
46746
+ method: "get",
46747
+ path: "/v1/workspaces/{id}/conversation",
46748
+ middleware: [requireScope("conversations:read")],
46749
+ request: {
46750
+ params: exports_external.object({ id: exports_external.uuid() }),
46751
+ query: ConversationListQuerySchema
46752
+ },
46753
+ responses: {
46754
+ 200: {
46755
+ description: "Durable conversation transcript for an active or terminal workspace",
46756
+ content: {
46757
+ "application/json": {
46758
+ schema: exports_external.object({
46759
+ items: exports_external.array(ConversationMessageResourceSchema),
46760
+ next_cursor: exports_external.number().int().positive().nullable(),
46761
+ retention: exports_external.object({
46762
+ status: exports_external.literal("retained"),
46763
+ expires_at: exports_external.iso.datetime().nullable()
46764
+ })
46765
+ })
46766
+ }
46767
+ }
46768
+ }
46769
+ }
46770
+ }), async (c) => {
46771
+ const principal = c.get("principal");
46772
+ const workspace2 = await service.getOwned(principal, c.req.valid("param").id);
46773
+ const state = await store2.getConversationState(workspace2.id);
46774
+ if (state?.status === "deleted") {
46775
+ throw new ApiError("conversation.deleted", "Conversation history was deleted.");
46776
+ }
46777
+ const expiresAt = state?.expiresAt ?? (workspace2.terminalAt ? new Date(workspace2.terminalAt.getTime() + parseDurationMs(workspace2.templateSnapshot.spec.persistence.conversationRetention)) : null);
46778
+ if (expiresAt && expiresAt <= new Date) {
46779
+ throw new ApiError("conversation.expired", "Conversation history has expired.");
46780
+ }
46781
+ const { after, limit } = c.req.valid("query");
46782
+ const rows = await store2.readConversation(workspace2.id, after, limit);
46783
+ const last = rows.at(-1);
46784
+ return c.json({
46785
+ items: rows.map((row) => ({
46786
+ message_id: row.messageId,
46787
+ seq: row.seq,
46788
+ role: row.role,
46789
+ content: row.content,
46790
+ occurred_at: row.occurredAt.toISOString(),
46791
+ metadata: row.metadata
46792
+ })),
46793
+ next_cursor: rows.length === limit && last ? last.seq : null,
46794
+ retention: { status: "retained", expires_at: expiresAt?.toISOString() ?? null }
46795
+ }, 200);
46796
+ });
46797
+ app.openapi(createRoute({
46798
+ method: "delete",
46799
+ path: "/v1/workspaces/{id}/conversation",
46800
+ middleware: [requireScope("conversations:delete")],
46801
+ request: { params: exports_external.object({ id: exports_external.uuid() }) },
46802
+ responses: { 204: { description: "Conversation content deleted idempotently" } }
46803
+ }), async (c) => {
46804
+ const principal = c.get("principal");
46805
+ const workspace2 = await service.getOwned(principal, c.req.valid("param").id);
46806
+ const at = new Date;
46807
+ const previous = await store2.getConversationState(workspace2.id);
46808
+ await store2.deleteConversation(workspace2.id, at);
46809
+ if (previous?.status !== "deleted") {
46810
+ await store2.appendEvent(workspace2.id, "workspace.conversation_deleted", {
46811
+ id: randomUUID11(),
46812
+ type: "workspace.conversation_deleted",
46813
+ occurred_at: at.toISOString(),
46814
+ workspace_id: workspace2.id
46815
+ }, at);
46816
+ }
46817
+ return c.body(null, 204);
46818
+ });
45369
46819
  app.openapi(createRoute({
45370
46820
  method: "get",
45371
46821
  path: "/v1/workspaces/{id}/outputs",
@@ -45480,13 +46930,7 @@ function buildServer(deps) {
45480
46930
  }), async (c) => {
45481
46931
  const principal = c.get("principal");
45482
46932
  const query = c.req.valid("query");
45483
- const rows = await store2.listWorkspaces(principal.id, {
45484
- ...query.external_id ? { externalId: query.external_id } : {},
45485
- ...query.state ? { state: query.state } : {},
45486
- ...query.template ? { template: query.template } : {},
45487
- limit: query.limit,
45488
- ...query.cursor ? { cursor: query.cursor } : {}
45489
- });
46933
+ const rows = await store2.listWorkspaces(principal.id, workspaceListFilterOf(query));
45490
46934
  const last = rows[rows.length - 1];
45491
46935
  return c.json({
45492
46936
  items: rows.map(toResource),
@@ -45609,7 +47053,15 @@ function buildServer(deps) {
45609
47053
  }, 200);
45610
47054
  });
45611
47055
  app.on(["GET", "POST", "PUT", "PATCH", "DELETE"], "/v1/workspaces/:id/services/:service/*", requireScope("services:relay"), relayHandler({ store: store2, hub, service }));
45612
- return { app, websocket: websocket2, hub, scheduler: scheduler2, service, persistence: persistence2 };
47056
+ return {
47057
+ app,
47058
+ websocket: websocket2,
47059
+ hub,
47060
+ scheduler: scheduler2,
47061
+ service,
47062
+ persistence: persistence2,
47063
+ ...warmPool ? { warmPool } : {}
47064
+ };
45613
47065
  }
45614
47066
 
45615
47067
  // ../../apps/pocketcoder-server/src/lifecycle.ts
@@ -45698,9 +47150,11 @@ async function startPocketcoderServer(config2 = loadConfig(), options = {}) {
45698
47150
  try {
45699
47151
  await loadConfiguredTemplates(store2, config2.templateDir, log);
45700
47152
  const driver2 = createWorkspaceDriver(config2);
47153
+ const warmPools = await resolveWarmPools(store2, config2.warmPools, driver2.kind, config2.limits.globalActiveWorkspaces);
47154
+ const warmPoolContinuously = warmPools.length > 0 || (await store2.listWarmPoolRuntimes()).some((runtime) => runtime.state !== "failed");
45701
47155
  const storageDriver = createStorageDriver(config2);
45702
47156
  const secretResolver = createSecretResolver(config2);
45703
- const { app, websocket: websocket2, scheduler: scheduler2, persistence: persistence2 } = buildServer({
47157
+ const { app, websocket: websocket2, scheduler: scheduler2, persistence: persistence2, warmPool } = buildServer({
45704
47158
  store: store2,
45705
47159
  driver: driver2,
45706
47160
  ...storageDriver ? { storageDriver } : {},
@@ -45710,7 +47164,8 @@ async function startPocketcoderServer(config2 = loadConfig(), options = {}) {
45710
47164
  workspaceServerUrl: config2.workspaceServerUrl,
45711
47165
  persistenceLimits: config2.persistenceLimits,
45712
47166
  ...options.instanceId ? { instanceId: options.instanceId } : {},
45713
- log
47167
+ log,
47168
+ warmPools
45714
47169
  });
45715
47170
  await reconcileStartup(store2, driver2, storageDriver, log);
45716
47171
  const outbox2 = new OutboxDispatcher({
@@ -45721,6 +47176,7 @@ async function startPocketcoderServer(config2 = loadConfig(), options = {}) {
45721
47176
  });
45722
47177
  const schedulerTimer = startExclusiveTimer(config2.schedulerIntervalMs, () => scheduler2.tick(), "scheduler tick failed", log);
45723
47178
  const outboxTimer = startExclusiveTimer(config2.outboxIntervalMs, () => outbox2.tick(), "outbox tick failed", log);
47179
+ const warmPoolTimer = warmPool && warmPoolContinuously ? startExclusiveTimer(config2.schedulerIntervalMs, () => warmPool.reconcile(), "warm pool reconciliation failed", log) : null;
45724
47180
  const retentionTimer = startExclusiveTimer(60000, async () => {
45725
47181
  const { deleted, skipped } = await persistence2.pruneExpired();
45726
47182
  if (deleted > 0 || skipped > 0) {
@@ -45739,12 +47195,16 @@ async function startPocketcoderServer(config2 = loadConfig(), options = {}) {
45739
47195
  clearInterval(schedulerTimer);
45740
47196
  clearInterval(outboxTimer);
45741
47197
  clearInterval(retentionTimer);
47198
+ if (warmPoolTimer)
47199
+ clearInterval(warmPoolTimer);
45742
47200
  throw error51;
45743
47201
  }
45744
47202
  const healthHost = config2.listenHost === "0.0.0.0" || config2.listenHost === "::" ? "127.0.0.1" : config2.listenHost;
45745
47203
  const url2 = `http://${healthHost}:${server.port}`;
45746
47204
  log(`listening on http://${config2.listenHost}:${server.port}`);
45747
47205
  log(`workspaces reach this server at ${config2.workspaceServerUrl}`);
47206
+ if (warmPool)
47207
+ warmPool.reconcile().catch((error51) => log(`warm pool initial reconcile failed: ${String(error51)}`));
45748
47208
  let stopPromise = null;
45749
47209
  return {
45750
47210
  config: config2,
@@ -45757,6 +47217,8 @@ async function startPocketcoderServer(config2 = loadConfig(), options = {}) {
45757
47217
  clearInterval(schedulerTimer);
45758
47218
  clearInterval(outboxTimer);
45759
47219
  clearInterval(retentionTimer);
47220
+ if (warmPoolTimer)
47221
+ clearInterval(warmPoolTimer);
45760
47222
  await server.stop(true);
45761
47223
  await store2.close();
45762
47224
  })();
@@ -45812,7 +47274,7 @@ function readState() {
45812
47274
  function writeState(state) {
45813
47275
  const path = statePath();
45814
47276
  mkdirSync(dirname5(path), { recursive: true, mode: 448 });
45815
- const temporary = `${path}.${process.pid}.${randomUUID10()}.tmp`;
47277
+ const temporary = `${path}.${process.pid}.${randomUUID12()}.tmp`;
45816
47278
  writeFileSync(temporary, `${JSON.stringify(state, null, 2)}
45817
47279
  `, { mode: 384 });
45818
47280
  renameSync(temporary, path);
@@ -45921,7 +47383,7 @@ async function startManagedServer(options) {
45921
47383
  if (existing)
45922
47384
  removeState();
45923
47385
  const config2 = loadConfig();
45924
- const instanceToken = randomUUID10();
47386
+ const instanceToken = randomUUID12();
45925
47387
  const state = {
45926
47388
  version: 1,
45927
47389
  pid: 0,
@@ -46311,7 +47773,7 @@ async function chatWorkspace(flags, deps) {
46311
47773
  }
46312
47774
 
46313
47775
  // src/workspace-create.ts
46314
- import { randomUUID as randomUUID11 } from "crypto";
47776
+ import { randomUUID as randomUUID13 } from "crypto";
46315
47777
  var TERMINAL_WORKSPACE_STATES = new Set([
46316
47778
  "succeeded",
46317
47779
  "failed",
@@ -46401,7 +47863,7 @@ async function waitForReady(initial, flags, deps) {
46401
47863
  }
46402
47864
  async function createWorkspace(flags, deps) {
46403
47865
  const template2 = need2(flags, "template", deps.fail);
46404
- const externalId = typeof flags["external-id"] === "string" ? flags["external-id"] : `pcd-${randomUUID11()}`;
47866
+ const externalId = typeof flags["external-id"] === "string" ? flags["external-id"] : `pcd-${randomUUID13()}`;
46405
47867
  const launchInput = parseLaunchInput(flags, deps.fail);
46406
47868
  const response = await deps.api("/v1/workspaces", {
46407
47869
  method: "POST",
@@ -46601,6 +48063,7 @@ function createCli(argv) {
46601
48063
  array: true,
46602
48064
  description: "Template manifest files"
46603
48065
  })).command("list", "List authorized template versions through the REST API", (command2) => command2.option("json", { type: "boolean", description: "Print JSON" })).command("list-database", "List every template version from PostgreSQL"));
48066
+ parser2 = commandGroup(parser2, "pools", "Inspect operator-managed warm capacity", (group) => group.command("list", "List warm pool inventory and metrics", (command2) => command2.option("json", { type: "boolean", description: "Print JSON" })));
46604
48067
  parser2 = commandGroup(parser2, "workspaces", "Manage workspaces", (group) => group.command("list", "List workspaces", (command2) => command2.option("active", {
46605
48068
  type: "boolean",
46606
48069
  description: "Only show nonterminal workspaces"
@@ -46885,6 +48348,25 @@ async function handleTemplates(context) {
46885
48348
  return false;
46886
48349
  }
46887
48350
  }
48351
+ async function handlePools(context) {
48352
+ if (context.group !== "pools" || context.action !== "list")
48353
+ return false;
48354
+ const response = await api2("/v1/warm-pools");
48355
+ const body = await response.json();
48356
+ if (!response.ok)
48357
+ fail(`warm pool inventory failed (${response.status}): ${JSON.stringify(body)}`);
48358
+ if (context.flags.json) {
48359
+ console.log(JSON.stringify(body, null, 2));
48360
+ return true;
48361
+ }
48362
+ for (const item of body.items ?? []) {
48363
+ const counts = Object.entries(item.counts).map(([state, count3]) => `${state}=${count3}`).join(" ");
48364
+ console.log(`${item.template}@${item.version} desired=${item.desired} ${counts || "empty"} oldest_ready_ms=${item.oldest_ready_age_ms ?? "-"}`);
48365
+ }
48366
+ if (body.metrics)
48367
+ console.log(`metrics ${Object.entries(body.metrics).map(([name2, value]) => `${name2}=${value}`).join(" ")}`);
48368
+ return true;
48369
+ }
46888
48370
  async function listWorkspaces(flags) {
46889
48371
  const params = new URLSearchParams;
46890
48372
  for (const [flag, param2] of [
@@ -46968,7 +48450,7 @@ async function handleWorkspacePersistence({
46968
48450
  };
46969
48451
  const res = await api2(`/v1/workspaces/${id}/preserve`, {
46970
48452
  method: "POST",
46971
- headers: { "idempotency-key": `preserve-${id}-${randomUUID12()}` },
48453
+ headers: { "idempotency-key": `preserve-${id}-${randomUUID14()}` },
46972
48454
  body: JSON.stringify(body)
46973
48455
  });
46974
48456
  console.log(JSON.stringify(await res.json(), null, 2));
@@ -47045,7 +48527,7 @@ async function verifyCheckpoint(flags) {
47045
48527
  const id = need3(flags, "id");
47046
48528
  const response = await api2(`/v1/checkpoints/${id}/verify`, {
47047
48529
  method: "POST",
47048
- headers: { "idempotency-key": `verify-${id}-${randomUUID12()}` }
48530
+ headers: { "idempotency-key": `verify-${id}-${randomUUID14()}` }
47049
48531
  });
47050
48532
  console.log(JSON.stringify(await response.json(), null, 2));
47051
48533
  if (!response.ok)
@@ -47156,7 +48638,7 @@ function containsDoctorNonce(message, nonce) {
47156
48638
  return !["user", "human"].includes(role) && JSON.stringify(message).includes(nonce);
47157
48639
  }
47158
48640
  async function runDoctorTurn(workspaceId, timeoutSeconds2) {
47159
- const nonce = `pocketcoder-doctor-${randomUUID12()}`;
48641
+ const nonce = `pocketcoder-doctor-${randomUUID14()}`;
47160
48642
  console.log("doctor: sending a correlated request/response probe");
47161
48643
  const messageResponse = await api2(`/v1/workspaces/${workspaceId}/services/agent/message`, {
47162
48644
  method: "POST",
@@ -47202,7 +48684,7 @@ async function handleDoctor({ group, flags }) {
47202
48684
  if (group !== "doctor")
47203
48685
  return false;
47204
48686
  const template2 = need3(flags, "template");
47205
- const externalId = `doctor-${randomUUID12()}`;
48687
+ const externalId = `doctor-${randomUUID14()}`;
47206
48688
  const turnTimeoutSeconds = doctorTurnTimeout(flags);
47207
48689
  console.log(`doctor: creating probe workspace from template ${template2}`);
47208
48690
  const createRes = await api2("/v1/workspaces", {
@@ -47244,6 +48726,7 @@ async function dispatchCommand(context) {
47244
48726
  handlePrincipals,
47245
48727
  handleKeys,
47246
48728
  handleTemplates,
48729
+ handlePools,
47247
48730
  handleWorkspaceCore,
47248
48731
  handleWorkspacePersistence,
47249
48732
  handleWorkspaceAttach,