@shawnstack/quickforge 1.3.20 → 1.3.22

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 (188) hide show
  1. package/README.md +348 -348
  2. package/dist/assets/{anthropic-Bj3HAZgj.js → anthropic-CDKnv1FQ.js} +1 -1
  3. package/dist/assets/{azure-openai-responses-IdZZrSrI.js → azure-openai-responses-BnUwVl-8.js} +1 -1
  4. package/dist/assets/{google-Brt_lS1J.js → google-DOEyCDZy.js} +1 -1
  5. package/dist/assets/{google-vertex-B6HsoZ34.js → google-vertex-BPPf3car.js} +1 -1
  6. package/dist/assets/{icons-BHkxP7oT.js → icons-WD3UkVNM.js} +1 -1
  7. package/dist/assets/{index-D0CVLdX_.js → index-CjTN0qaQ.js} +586 -554
  8. package/dist/assets/index-eeLjaV06.css +3 -0
  9. package/dist/assets/{mistral-CenXqwPz.js → mistral-Ber29mja.js} +1 -1
  10. package/dist/assets/{openai-codex-responses-D9ffGwbj.js → openai-codex-responses-D8gq8a3l.js} +1 -1
  11. package/dist/assets/{openai-completions-eWdeSGBG.js → openai-completions-CATWPFBp.js} +1 -1
  12. package/dist/assets/{openai-responses-Cavpmjeu.js → openai-responses-DxcB6Ksu.js} +1 -1
  13. package/dist/assets/{openai-responses-shared-DF3ZGaUx.js → openai-responses-shared-a_PAPxTO.js} +1 -1
  14. package/dist/assets/{react-vendor-CmyL2roG.js → react-vendor-BcQaTQ90.js} +1 -1
  15. package/dist/index.html +4 -4
  16. package/node_modules/@aws-sdk/client-bedrock-runtime/dist-cjs/index.js +1 -0
  17. package/node_modules/@aws-sdk/client-bedrock-runtime/dist-es/models/enums.js +1 -0
  18. package/node_modules/@aws-sdk/client-bedrock-runtime/package.json +11 -11
  19. package/node_modules/@aws-sdk/core/package.json +3 -3
  20. package/node_modules/@aws-sdk/credential-provider-env/package.json +3 -3
  21. package/node_modules/@aws-sdk/credential-provider-http/package.json +5 -5
  22. package/node_modules/@aws-sdk/credential-provider-ini/package.json +11 -11
  23. package/node_modules/@aws-sdk/credential-provider-login/package.json +4 -4
  24. package/node_modules/@aws-sdk/credential-provider-node/package.json +9 -9
  25. package/node_modules/@aws-sdk/credential-provider-process/package.json +3 -3
  26. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/LICENSE +201 -0
  27. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/README.md +62 -0
  28. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-cjs/index.js +156 -0
  29. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/constants.js +2 -0
  30. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromEnvSigningName.js +16 -0
  31. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromSso.js +80 -0
  32. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromStatic.js +8 -0
  33. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/getNewSsoOidcToken.js +11 -0
  34. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/getSsoOidcClient.js +10 -0
  35. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/index.js +4 -0
  36. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/nodeProvider.js +5 -0
  37. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/validateTokenExpiry.js +7 -0
  38. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/validateTokenKey.js +7 -0
  39. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/writeSSOTokenToFile.js +8 -0
  40. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/package.json +69 -0
  41. package/node_modules/@aws-sdk/credential-provider-sso/package.json +5 -5
  42. package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +4 -4
  43. package/node_modules/@aws-sdk/eventstream-handler-node/package.json +2 -2
  44. package/node_modules/@aws-sdk/middleware-eventstream/package.json +2 -2
  45. package/node_modules/@aws-sdk/middleware-websocket/package.json +5 -5
  46. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +1 -1
  47. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +1 -1
  48. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +1 -1
  49. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +1 -1
  50. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +1 -1
  51. package/node_modules/@aws-sdk/nested-clients/package.json +6 -6
  52. package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +2 -2
  53. package/node_modules/@aws-sdk/token-providers/package.json +4 -4
  54. package/node_modules/@nodable/entities/package.json +1 -1
  55. package/node_modules/@nodable/entities/src/EntityDecoder.js +1 -1
  56. package/node_modules/@nodable/entities/src/entities.js +0 -18
  57. package/node_modules/@smithy/core/dist-cjs/index.js +3 -4
  58. package/node_modules/@smithy/core/dist-cjs/submodules/client/index.js +3 -11
  59. package/node_modules/@smithy/core/dist-cjs/submodules/config/index.browser.js +2 -2
  60. package/node_modules/@smithy/core/dist-cjs/submodules/config/index.js +2 -2
  61. package/node_modules/@smithy/core/dist-cjs/submodules/config/index.native.js +2 -2
  62. package/node_modules/@smithy/core/dist-cjs/submodules/endpoints/index.browser.js +9 -40
  63. package/node_modules/@smithy/core/dist-cjs/submodules/endpoints/index.js +9 -40
  64. package/node_modules/@smithy/core/dist-cjs/submodules/protocols/index.js +12 -142
  65. package/node_modules/@smithy/core/dist-cjs/submodules/schema/index.js +7 -9
  66. package/node_modules/@smithy/core/dist-cjs/submodules/serde/index.browser.js +2 -2
  67. package/node_modules/@smithy/core/dist-cjs/submodules/serde/index.js +2 -2
  68. package/node_modules/@smithy/core/dist-cjs/submodules/serde/index.native.js +2 -2
  69. package/node_modules/@smithy/core/dist-cjs/submodules/transport/index.js +184 -0
  70. package/node_modules/@smithy/core/dist-es/index.js +6 -6
  71. package/node_modules/@smithy/core/dist-es/submodules/client/index.js +2 -2
  72. package/node_modules/@smithy/core/dist-es/submodules/config/config-resolver/regionConfig/checkRegion.js +1 -1
  73. package/node_modules/@smithy/core/dist-es/submodules/endpoints/index.browser.js +2 -2
  74. package/node_modules/@smithy/core/dist-es/submodules/endpoints/index.js +2 -2
  75. package/node_modules/@smithy/core/dist-es/submodules/endpoints/middleware-endpoint/adaptors/toEndpointV1.js +1 -1
  76. package/node_modules/@smithy/core/dist-es/submodules/endpoints/middleware-endpoint/resolveEndpointConfig.js +1 -1
  77. package/node_modules/@smithy/core/dist-es/submodules/endpoints/util-endpoints/lib/index.js +1 -1
  78. package/node_modules/@smithy/core/dist-es/submodules/protocols/HttpBindingProtocol.js +1 -1
  79. package/node_modules/@smithy/core/dist-es/submodules/protocols/HttpProtocol.js +1 -2
  80. package/node_modules/@smithy/core/dist-es/submodules/protocols/RpcProtocol.js +1 -1
  81. package/node_modules/@smithy/core/dist-es/submodules/protocols/index.js +5 -5
  82. package/node_modules/@smithy/core/dist-es/submodules/protocols/middleware-content-length/contentLengthMiddleware.js +1 -1
  83. package/node_modules/@smithy/core/dist-es/submodules/protocols/requestBuilder.js +1 -1
  84. package/node_modules/@smithy/core/dist-es/submodules/schema/middleware/schemaDeserializationMiddleware.js +3 -4
  85. package/node_modules/@smithy/core/dist-es/submodules/schema/middleware/schemaSerializationMiddleware.js +1 -2
  86. package/node_modules/@smithy/core/dist-es/submodules/serde/middleware-serde/deserializerMiddleware.js +1 -1
  87. package/node_modules/@smithy/core/dist-es/submodules/transport/index.js +9 -0
  88. package/node_modules/@smithy/core/dist-es/submodules/{protocols/url-parser → transport}/parseUrl.js +1 -1
  89. package/node_modules/@smithy/core/dist-es/submodules/{endpoints → transport}/toEndpointV1.js +1 -1
  90. package/node_modules/@smithy/core/package.json +19 -10
  91. package/node_modules/@smithy/core/transport.js +5 -0
  92. package/node_modules/@smithy/credential-provider-imds/dist-cjs/index.js +14 -13
  93. package/node_modules/@smithy/credential-provider-imds/dist-es/fromContainerMetadata.js +14 -13
  94. package/node_modules/@smithy/credential-provider-imds/package.json +2 -2
  95. package/node_modules/@smithy/fetch-http-handler/package.json +2 -2
  96. package/node_modules/@smithy/node-http-handler/package.json +2 -2
  97. package/node_modules/@smithy/signature-v4/package.json +2 -2
  98. package/node_modules/eventsource-parser/README.md +31 -0
  99. package/node_modules/eventsource-parser/dist/index.cjs +21 -10
  100. package/node_modules/eventsource-parser/dist/index.js +21 -10
  101. package/node_modules/eventsource-parser/dist/stream.cjs +4 -3
  102. package/node_modules/eventsource-parser/dist/stream.js +4 -3
  103. package/node_modules/eventsource-parser/package.json +8 -8
  104. package/node_modules/hasown/CHANGELOG.md +7 -0
  105. package/node_modules/hasown/package.json +4 -5
  106. package/node_modules/protobufjs/dist/light/protobuf.js +7 -5
  107. package/node_modules/protobufjs/dist/light/protobuf.min.js +3 -3
  108. package/node_modules/protobufjs/dist/minimal/protobuf.js +3 -3
  109. package/node_modules/protobufjs/dist/minimal/protobuf.min.js +3 -3
  110. package/node_modules/protobufjs/dist/protobuf.js +7 -5
  111. package/node_modules/protobufjs/dist/protobuf.min.js +3 -3
  112. package/node_modules/protobufjs/package.json +1 -1
  113. package/node_modules/protobufjs/src/converter.js +4 -2
  114. package/node_modules/protobufjs/src/roots.js +1 -1
  115. package/node_modules/typebox/build/type/script/mapping.mjs +15 -8
  116. package/node_modules/typebox/build/type/script/parser.mjs +2 -1
  117. package/node_modules/typebox/package.json +29 -29
  118. package/package.json +1 -1
  119. package/server/agent-manager.mjs +63 -25
  120. package/server/agent-profiles.mjs +191 -0
  121. package/server/auto-compaction.mjs +20 -0
  122. package/server/index.mjs +32 -8
  123. package/server/mcp/registry.mjs +13 -2
  124. package/server/routes/agent-profiles.mjs +172 -0
  125. package/server/routes/lan-access.mjs +20 -0
  126. package/server/routes/scheduled-tasks.mjs +161 -47
  127. package/server/routes/shared-conversation.mjs +14 -0
  128. package/server/routes/storage.mjs +10 -0
  129. package/server/routes/terminal.mjs +13 -3
  130. package/server/session-utils.mjs +2 -2
  131. package/server/storage.mjs +3 -4
  132. package/server/system-prompt.mjs +10 -5
  133. package/server/terminal/terminal-manager.mjs +9 -1
  134. package/server/tools/definitions.mjs +2 -7
  135. package/server/utils/response.mjs +4 -0
  136. package/dist/assets/index-D0W9hAl_.css +0 -3
  137. package/node_modules/@smithy/core/dist-cjs/getSmithyContext.js +0 -6
  138. package/node_modules/@smithy/core/dist-cjs/middleware-http-auth-scheme/getHttpAuthSchemeEndpointRuleSetPlugin.js +0 -21
  139. package/node_modules/@smithy/core/dist-cjs/middleware-http-auth-scheme/getHttpAuthSchemePlugin.js +0 -21
  140. package/node_modules/@smithy/core/dist-cjs/middleware-http-auth-scheme/httpAuthSchemeMiddleware.js +0 -46
  141. package/node_modules/@smithy/core/dist-cjs/middleware-http-auth-scheme/index.js +0 -6
  142. package/node_modules/@smithy/core/dist-cjs/middleware-http-auth-scheme/resolveAuthOptions.js +0 -24
  143. package/node_modules/@smithy/core/dist-cjs/middleware-http-signing/getHttpSigningMiddleware.js +0 -19
  144. package/node_modules/@smithy/core/dist-cjs/middleware-http-signing/httpSigningMiddleware.js +0 -27
  145. package/node_modules/@smithy/core/dist-cjs/middleware-http-signing/index.js +0 -5
  146. package/node_modules/@smithy/core/dist-cjs/normalizeProvider.js +0 -10
  147. package/node_modules/@smithy/core/dist-cjs/pagination/createPaginator.js +0 -44
  148. package/node_modules/@smithy/core/dist-cjs/request-builder/requestBuilder.js +0 -5
  149. package/node_modules/@smithy/core/dist-cjs/setFeature.js +0 -14
  150. package/node_modules/@smithy/core/dist-cjs/util-identity-and-auth/DefaultIdentityProviderConfig.js +0 -18
  151. package/node_modules/@smithy/core/dist-cjs/util-identity-and-auth/httpAuthSchemes/httpApiKeyAuth.js +0 -38
  152. package/node_modules/@smithy/core/dist-cjs/util-identity-and-auth/httpAuthSchemes/httpBearerAuth.js +0 -15
  153. package/node_modules/@smithy/core/dist-cjs/util-identity-and-auth/httpAuthSchemes/index.js +0 -6
  154. package/node_modules/@smithy/core/dist-cjs/util-identity-and-auth/httpAuthSchemes/noAuth.js +0 -9
  155. package/node_modules/@smithy/core/dist-cjs/util-identity-and-auth/index.js +0 -6
  156. package/node_modules/@smithy/core/dist-cjs/util-identity-and-auth/memoizeIdentityProvider.js +0 -61
  157. package/node_modules/@smithy/core/dist-es/request-builder/requestBuilder.js +0 -1
  158. package/node_modules/@smithy/core/dist-es/submodules/client/util-middleware/getSmithyContext.js +0 -2
  159. package/node_modules/@smithy/core/dist-es/submodules/event-streams/eventstream-codec/TestVectors.fixture.js +0 -146
  160. package/node_modules/@smithy/core/dist-es/submodules/event-streams/eventstream-codec/vectorTypes.fixture.js +0 -1
  161. package/node_modules/@smithy/credential-provider-imds/dist-es/remoteProvider/index.js +0 -2
  162. package/node_modules/@smithy/node-http-handler/dist-es/readable.mock.js +0 -21
  163. package/node_modules/@smithy/node-http-handler/dist-es/server.mock.js +0 -88
  164. package/node_modules/@smithy/node-http-handler/dist-es/stream-collector/readable.mock.js +0 -21
  165. package/node_modules/@smithy/signature-v4/dist-es/suite.fixture.js +0 -399
  166. /package/node_modules/@smithy/core/dist-es/{middleware-http-auth-scheme → legacy-root-exports/middleware-http-auth-scheme}/getHttpAuthSchemeEndpointRuleSetPlugin.js +0 -0
  167. /package/node_modules/@smithy/core/dist-es/{middleware-http-auth-scheme → legacy-root-exports/middleware-http-auth-scheme}/getHttpAuthSchemePlugin.js +0 -0
  168. /package/node_modules/@smithy/core/dist-es/{middleware-http-auth-scheme → legacy-root-exports/middleware-http-auth-scheme}/httpAuthSchemeMiddleware.js +0 -0
  169. /package/node_modules/@smithy/core/dist-es/{middleware-http-auth-scheme → legacy-root-exports/middleware-http-auth-scheme}/index.js +0 -0
  170. /package/node_modules/@smithy/core/dist-es/{middleware-http-auth-scheme → legacy-root-exports/middleware-http-auth-scheme}/resolveAuthOptions.js +0 -0
  171. /package/node_modules/@smithy/core/dist-es/{middleware-http-signing → legacy-root-exports/middleware-http-signing}/getHttpSigningMiddleware.js +0 -0
  172. /package/node_modules/@smithy/core/dist-es/{middleware-http-signing → legacy-root-exports/middleware-http-signing}/httpSigningMiddleware.js +0 -0
  173. /package/node_modules/@smithy/core/dist-es/{middleware-http-signing → legacy-root-exports/middleware-http-signing}/index.js +0 -0
  174. /package/node_modules/@smithy/core/dist-es/{pagination → legacy-root-exports/pagination}/createPaginator.js +0 -0
  175. /package/node_modules/@smithy/core/dist-es/{util-identity-and-auth → legacy-root-exports/util-identity-and-auth}/DefaultIdentityProviderConfig.js +0 -0
  176. /package/node_modules/@smithy/core/dist-es/{util-identity-and-auth → legacy-root-exports/util-identity-and-auth}/httpAuthSchemes/httpApiKeyAuth.js +0 -0
  177. /package/node_modules/@smithy/core/dist-es/{util-identity-and-auth → legacy-root-exports/util-identity-and-auth}/httpAuthSchemes/httpBearerAuth.js +0 -0
  178. /package/node_modules/@smithy/core/dist-es/{util-identity-and-auth → legacy-root-exports/util-identity-and-auth}/httpAuthSchemes/index.js +0 -0
  179. /package/node_modules/@smithy/core/dist-es/{util-identity-and-auth → legacy-root-exports/util-identity-and-auth}/httpAuthSchemes/noAuth.js +0 -0
  180. /package/node_modules/@smithy/core/dist-es/{util-identity-and-auth → legacy-root-exports/util-identity-and-auth}/index.js +0 -0
  181. /package/node_modules/@smithy/core/dist-es/{util-identity-and-auth → legacy-root-exports/util-identity-and-auth}/memoizeIdentityProvider.js +0 -0
  182. /package/node_modules/@smithy/core/dist-es/{getSmithyContext.js → submodules/transport/getSmithyContext.js} +0 -0
  183. /package/node_modules/@smithy/core/dist-es/submodules/{protocols/protocol-http → transport}/httpRequest.js +0 -0
  184. /package/node_modules/@smithy/core/dist-es/submodules/{protocols/protocol-http → transport}/httpResponse.js +0 -0
  185. /package/node_modules/@smithy/core/dist-es/submodules/{endpoints/util-endpoints/lib → transport}/isValidHostLabel.js +0 -0
  186. /package/node_modules/@smithy/core/dist-es/submodules/{protocols/protocol-http → transport}/isValidHostname.js +0 -0
  187. /package/node_modules/@smithy/core/dist-es/submodules/{client/util-middleware → transport}/normalizeProvider.js +0 -0
  188. /package/node_modules/@smithy/core/dist-es/submodules/{protocols/querystring-parser → transport}/parseQueryString.js +0 -0
@@ -9,6 +9,7 @@ const TOOL_PREFIX = 'mcp__'
9
9
  const CONNECT_TIMEOUT_MS = 15_000
10
10
  const CALL_TIMEOUT_MS = 120_000
11
11
  const MAX_TEXT_LENGTH = 60_000
12
+ const RETRY_ERROR_AFTER_MS = 30_000
12
13
 
13
14
  const connections = new Map()
14
15
  let refreshPromise = null
@@ -126,6 +127,7 @@ async function connectServer(config) {
126
127
  error: null,
127
128
  tools: [],
128
129
  connectedAt: null,
130
+ lastAttemptAt: Date.now(),
129
131
  stderr: '',
130
132
  }
131
133
 
@@ -133,7 +135,7 @@ async function connectServer(config) {
133
135
  connection.stderr = truncateText(connection.stderr + chunk.toString(), 4000)
134
136
  })
135
137
  transport.onclose = () => {
136
- connection.status = 'disconnected'
138
+ if (connection.status !== 'error') connection.status = 'disconnected'
137
139
  }
138
140
  transport.onerror = (error) => {
139
141
  connection.status = 'error'
@@ -189,7 +191,15 @@ async function refreshConnections() {
189
191
  }
190
192
 
191
193
  for (const config of enabled.values()) {
192
- if (connections.has(config.name)) continue
194
+ const existing = connections.get(config.name)
195
+ if (existing) {
196
+ if (existing.status === 'error' && Date.now() - (existing.lastAttemptAt || 0) >= RETRY_ERROR_AFTER_MS) {
197
+ connections.delete(config.name)
198
+ await closeConnection(existing)
199
+ } else {
200
+ continue
201
+ }
202
+ }
193
203
  try {
194
204
  const connection = await connectServer(config)
195
205
  connections.set(config.name, connection)
@@ -204,6 +214,7 @@ async function refreshConnections() {
204
214
  tools: [],
205
215
  connectedAt: null,
206
216
  stderr: '',
217
+ lastAttemptAt: Date.now(),
207
218
  })
208
219
  }
209
220
  }
@@ -0,0 +1,172 @@
1
+ import { streamSimple } from '@mariozechner/pi-ai'
2
+ import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
3
+ import { readStore } from '../storage.mjs'
4
+ import { logger } from '../utils/logger.mjs'
5
+ import {
6
+ createCustomAgentProfile,
7
+ deleteCustomAgentProfile,
8
+ getAgentProfile,
9
+ listAgentProfiles,
10
+ listAvailableAgentTools,
11
+ updateCustomAgentProfile,
12
+ } from '../agent-profiles.mjs'
13
+
14
+ function requestError(message, statusCode = 400) {
15
+ const error = new Error(message)
16
+ error.statusCode = statusCode
17
+ return error
18
+ }
19
+
20
+ function normalizeAiJson(text) {
21
+ const raw = String(text || '').trim()
22
+ const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)
23
+ const candidate = fenced?.[1] ?? raw
24
+ const start = candidate.indexOf('{')
25
+ const end = candidate.lastIndexOf('}')
26
+ if (start < 0 || end < start) return null
27
+ try {
28
+ return JSON.parse(candidate.slice(start, end + 1))
29
+ } catch {
30
+ return null
31
+ }
32
+ }
33
+
34
+ async function getApiKey(provider) {
35
+ try {
36
+ const keys = await readStore('provider-keys')
37
+ return keys?.[provider] || undefined
38
+ } catch {
39
+ return undefined
40
+ }
41
+ }
42
+
43
+ function normalizeGeneratedName(value) {
44
+ const raw = String(value || '')
45
+ .trim()
46
+ .toLowerCase()
47
+ .replace(/\s+/g, '_')
48
+ .replace(/[^a-z0-9_-]/g, '')
49
+ .slice(0, 40)
50
+ const normalized = /^[a-z][a-z0-9_-]{1,39}$/.test(raw) && raw !== 'general' && raw !== 'explore' ? raw : ''
51
+ if (!normalized) throw requestError('AI did not generate a valid agent name', 502)
52
+ return normalized
53
+ }
54
+
55
+ function normalizeGeneratedAgentProfile(value) {
56
+ const name = normalizeGeneratedName(value?.name)
57
+ const label = String(value?.label || '').trim().slice(0, 80)
58
+ const description = String(value?.description || '').trim().slice(0, 500)
59
+ const systemPrompt = String(value?.systemPrompt || '').trim()
60
+ if (!label) throw requestError('AI did not generate a display name', 502)
61
+ if (!systemPrompt) throw requestError('AI did not generate a system prompt', 502)
62
+ return { name, label, description, systemPrompt }
63
+ }
64
+
65
+ async function generateAgentProfileWithAi(instruction, model, thinkingLevel = 'off') {
66
+ const text = String(instruction || '').trim()
67
+ if (!text) throw requestError('Please describe the agent you want to create')
68
+ if (!model) throw requestError('Please configure a default model first')
69
+
70
+ const systemPrompt = `You are a QuickForge Agent Profile generator.
71
+ Generate only the basic definition fields for a custom Agent Profile from the user's request.
72
+
73
+ Return JSON only. Do not use Markdown. Do not explain.
74
+
75
+ Required JSON shape:
76
+ {
77
+ "name": "lowercase identifier, starts with a letter, 2-40 chars, only lowercase letters, numbers, underscores, hyphens",
78
+ "label": "short display name",
79
+ "description": "one concise sentence describing the agent purpose",
80
+ "systemPrompt": "complete system prompt with role, scope, workflow, boundaries, and output expectations"
81
+ }
82
+
83
+ Rules:
84
+ - Do not include allowedTools, maxRuntimeMs, maxToolCalls, enabledAsSubagent, or any other fields.
85
+ - name must be English-like lowercase ASCII and must not be general or explore.
86
+ - systemPrompt should be specific and actionable.
87
+ - If the user requests Chinese, write label, description, and systemPrompt in Chinese; otherwise match the user's language.`
88
+
89
+ try {
90
+ const stream = streamSimple(
91
+ model,
92
+ {
93
+ systemPrompt,
94
+ messages: [{ role: 'user', content: text, timestamp: Date.now() }],
95
+ tools: [],
96
+ },
97
+ {
98
+ apiKey: await getApiKey(model.provider),
99
+ maxTokens: 1600,
100
+ temperature: 0,
101
+ reasoning: thinkingLevel === 'off' ? undefined : thinkingLevel,
102
+ maxRetryDelayMs: 60000,
103
+ },
104
+ )
105
+ const message = await stream.result()
106
+ const content = Array.isArray(message.content)
107
+ ? message.content.filter((block) => block.type === 'text').map((block) => block.text ?? '').join('\n')
108
+ : ''
109
+ const parsed = normalizeAiJson(content)
110
+ if (!parsed) throw requestError('AI did not return valid JSON', 502)
111
+ return normalizeGeneratedAgentProfile(parsed)
112
+ } catch (error) {
113
+ if (error?.statusCode) throw error
114
+ logger.warn('AI agent profile generation failed:', error?.message || error)
115
+ throw requestError(`AI generation failed: ${error?.message || 'check model configuration and API key'}`, 502)
116
+ }
117
+ }
118
+
119
+ export async function handleAgentProfilesApi(req, res, url) {
120
+ const parts = url.pathname.split('/').filter(Boolean)
121
+
122
+ if (req.method === 'GET' && url.pathname === '/api/agent-profiles') {
123
+ sendJson(res, 200, { agents: await listAgentProfiles({ includeDisabled: true }) })
124
+ return
125
+ }
126
+
127
+ if (req.method === 'GET' && url.pathname === '/api/agent-profiles/available-tools') {
128
+ sendJson(res, 200, { tools: listAvailableAgentTools() })
129
+ return
130
+ }
131
+
132
+ if (req.method === 'POST' && url.pathname === '/api/agent-profiles/ai-fill') {
133
+ const body = await readJsonBody(req)
134
+ sendJson(res, 200, { agent: await generateAgentProfileWithAi(body?.instruction, body?.model, body?.thinkingLevel) })
135
+ return
136
+ }
137
+
138
+ if (req.method === 'POST' && url.pathname === '/api/agent-profiles') {
139
+ const body = await readJsonBody(req)
140
+ sendJson(res, 200, { agent: await createCustomAgentProfile(body || {}) })
141
+ return
142
+ }
143
+
144
+ if (parts[0] === 'api' && parts[1] === 'agent-profiles' && parts[2]) {
145
+ const id = decodeSegment(parts[2])
146
+
147
+ if (req.method === 'GET') {
148
+ const agent = await getAgentProfile(id)
149
+ if (!agent) throw requestError('Agent not found', 404)
150
+ sendJson(res, 200, { agent })
151
+ return
152
+ }
153
+
154
+ if (req.method === 'PATCH' || req.method === 'PUT') {
155
+ const current = await getAgentProfile(id)
156
+ if (current?.builtin) throw requestError('Built-in agents cannot be modified', 403)
157
+ const body = await readJsonBody(req)
158
+ sendJson(res, 200, { agent: await updateCustomAgentProfile(id, body || {}) })
159
+ return
160
+ }
161
+
162
+ if (req.method === 'DELETE') {
163
+ const current = await getAgentProfile(id)
164
+ if (current?.builtin) throw requestError('Built-in agents cannot be deleted', 403)
165
+ await deleteCustomAgentProfile(id)
166
+ sendJson(res, 200, { ok: true })
167
+ return
168
+ }
169
+ }
170
+
171
+ throw requestError('Not found', 404)
172
+ }
@@ -12,7 +12,26 @@ import {
12
12
  const MAX_FAILED_ATTEMPTS = 5
13
13
  const ATTEMPT_WINDOW_MS = 5 * 60 * 1000
14
14
  const LOCK_MS = 5 * 60 * 1000
15
+ const ATTEMPT_CLEANUP_MS = 5 * 60 * 1000
15
16
  const attempts = new Map()
17
+ let cleanupTimer = null
18
+
19
+ function cleanupAttempts() {
20
+ const now = Date.now()
21
+ for (const [key, state] of attempts) {
22
+ if (state.resetAt <= now && state.lockedUntil <= now) attempts.delete(key)
23
+ }
24
+ if (attempts.size === 0 && cleanupTimer) {
25
+ clearInterval(cleanupTimer)
26
+ cleanupTimer = null
27
+ }
28
+ }
29
+
30
+ function scheduleAttemptCleanup() {
31
+ if (cleanupTimer) return
32
+ cleanupTimer = setInterval(cleanupAttempts, ATTEMPT_CLEANUP_MS)
33
+ cleanupTimer.unref?.()
34
+ }
16
35
 
17
36
  function remoteKey(req) {
18
37
  return String(req.socket.remoteAddress || 'unknown')
@@ -25,6 +44,7 @@ function attemptState(req) {
25
44
  if (!state || state.resetAt <= now) {
26
45
  const fresh = { count: 0, resetAt: now + ATTEMPT_WINDOW_MS, lockedUntil: 0 }
27
46
  attempts.set(key, fresh)
47
+ scheduleAttemptCleanup()
28
48
  return fresh
29
49
  }
30
50
  return state
@@ -2,6 +2,7 @@ import { streamSimple } from '@mariozechner/pi-ai'
2
2
  import { readJsonBody, sendJson, decodeSegment } from '../utils/response.mjs'
3
3
  import { readStore, atomicUpdate } from '../storage.mjs'
4
4
  import { createAgent, getSessionEventBus, agentEvents, persistSessionState } from '../agent-manager.mjs'
5
+ import { agentProfileSnapshot, getAgentProfile } from '../agent-profiles.mjs'
5
6
  import { projectContextFromId, readProjectConfig } from '../project-config.mjs'
6
7
  import { logger } from '../utils/logger.mjs'
7
8
 
@@ -17,7 +18,55 @@ const weekDayNames = ['周日', '周一', '周二', '周三', '周四', '周五'
17
18
 
18
19
  let schedulerTimer = null
19
20
  let running = false
20
- const runningTaskIds = new Set()
21
+ const runningTaskRunIds = new Map()
22
+
23
+ function executionModeFor(task) {
24
+ return task?.executionMode === 'parallel' ? 'parallel' : 'serial'
25
+ }
26
+
27
+ function normalizeExecutionMode(value) {
28
+ if (value === undefined || value === null || value === '') return 'serial'
29
+ const mode = String(value)
30
+ if (mode === 'serial' || mode === 'parallel') return mode
31
+ throw requestError('executionMode must be serial or parallel')
32
+ }
33
+
34
+ function currentRunIdsFor(task) {
35
+ const ids = []
36
+ if (Array.isArray(task?.currentRunIds)) ids.push(...task.currentRunIds.filter(Boolean))
37
+ if (task?.currentRunId) ids.push(task.currentRunId)
38
+ return [...new Set(ids)]
39
+ }
40
+
41
+ function activeRunIdsFor(task) {
42
+ const runningIds = [...(runningTaskRunIds.get(task?.id) || [])]
43
+ return [...new Set([...runningIds, ...currentRunIdsFor(task)])]
44
+ }
45
+
46
+ function hasActiveTaskRuns(task) {
47
+ return activeRunIdsFor(task).length > 0
48
+ }
49
+
50
+ function addActiveRun(taskId, runId) {
51
+ const ids = runningTaskRunIds.get(taskId) || new Set()
52
+ ids.add(runId)
53
+ runningTaskRunIds.set(taskId, ids)
54
+ }
55
+
56
+ function removeActiveRun(taskId, runId) {
57
+ const ids = runningTaskRunIds.get(taskId)
58
+ if (!ids) return
59
+ ids.delete(runId)
60
+ if (ids.size === 0) runningTaskRunIds.delete(taskId)
61
+ }
62
+
63
+ function appendCurrentRunId(task, runId) {
64
+ return [...new Set([...currentRunIdsFor(task), runId])]
65
+ }
66
+
67
+ function removeCurrentRunId(task, runId) {
68
+ return currentRunIdsFor(task).filter((id) => id !== runId)
69
+ }
21
70
 
22
71
  function createId() {
23
72
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
@@ -260,6 +309,8 @@ function normalizeTaskInput(input, existing = {}) {
260
309
  const title = nonEmptyString(input?.title ?? existing.title, 'title').slice(0, 80)
261
310
  const instruction = nonEmptyString(input?.instruction ?? existing.instruction, 'instruction')
262
311
  const scheduleType = String(input?.scheduleType ?? existing.scheduleType ?? 'daily')
312
+ const agentId = Object.prototype.hasOwnProperty.call(input || {}, 'agentId') ? (input.agentId || null) : (existing.agentId || null)
313
+ const executionMode = normalizeExecutionMode(input?.executionMode ?? existing.executionMode)
263
314
 
264
315
  if (scheduleType === 'cron') {
265
316
  const cronExpression = String(input?.cronExpression ?? existing.cronExpression ?? '').trim()
@@ -269,6 +320,8 @@ function normalizeTaskInput(input, existing = {}) {
269
320
  return {
270
321
  title,
271
322
  instruction,
323
+ agentId,
324
+ executionMode,
272
325
  scheduleType: 'cron',
273
326
  scheduleRule: String(input?.scheduleRule ?? existing.scheduleRule ?? cronExpression).trim(),
274
327
  cronExpression,
@@ -288,6 +341,8 @@ function normalizeTaskInput(input, existing = {}) {
288
341
  return {
289
342
  title,
290
343
  instruction,
344
+ agentId,
345
+ executionMode,
291
346
  scheduleType,
292
347
  scheduleRule: `单次 ${formatLocalDateTime(executeAt)}`,
293
348
  cronExpression: undefined,
@@ -306,6 +361,8 @@ function normalizeTaskInput(input, existing = {}) {
306
361
  return {
307
362
  title,
308
363
  instruction,
364
+ agentId,
365
+ executionMode,
309
366
  scheduleType,
310
367
  scheduleRule: `每天 ${executeTime}`,
311
368
  cronExpression: undefined,
@@ -323,6 +380,8 @@ function normalizeTaskInput(input, existing = {}) {
323
380
  return {
324
381
  title,
325
382
  instruction,
383
+ agentId,
384
+ executionMode,
326
385
  scheduleType,
327
386
  scheduleRule: `每${weekDayNames[weekDay]} ${executeTime}`,
328
387
  cronExpression: undefined,
@@ -339,6 +398,8 @@ function normalizeTaskInput(input, existing = {}) {
339
398
  return {
340
399
  title,
341
400
  instruction,
401
+ agentId,
402
+ executionMode,
342
403
  scheduleType,
343
404
  scheduleRule: `每月 ${monthDay} 号 ${executeTime}`,
344
405
  cronExpression: undefined,
@@ -514,27 +575,56 @@ async function resolveExecutionProject(task) {
514
575
  }
515
576
 
516
577
  async function executeTask(task, trigger = 'schedule', onStarted) {
517
- if (runningTaskIds.has(task.id)) return
518
- runningTaskIds.add(task.id)
578
+ const mode = executionModeFor(task)
579
+ const advanceNextRunAtAtStart = trigger === 'schedule' && mode === 'parallel'
580
+ if (mode === 'serial' && hasActiveTaskRuns(task)) return
519
581
  const runId = createId()
582
+ addActiveRun(task.id, runId)
520
583
  const startedAt = new Date().toISOString()
521
584
  const scheduledAt = task.nextRunAt
522
585
  let sessionId = `scheduled-${task.id}-${Date.now().toString(36)}`
523
-
524
- await updateTask(task.id, (current) => ({
525
- ...current,
526
- currentRunId: runId,
527
- lastSessionId: sessionId,
528
- runs: [{
529
- id: runId,
530
- status: 'running',
531
- trigger,
532
- inputContent: current.instruction,
533
- sessionId,
534
- scheduledAt,
535
- startedAt,
536
- }, ...(current.runs || [])].slice(0, MAX_RUN_HISTORY_PER_TASK),
537
- }))
586
+ let executionAgent = null
587
+ let agentWarning = null
588
+ if (task.agentId) {
589
+ executionAgent = await getAgentProfile(task.agentId)
590
+ if (!executionAgent) agentWarning = `Configured agent not found: ${task.agentId}`
591
+ }
592
+ const agentSnapshot = executionAgent ? agentProfileSnapshot(executionAgent) : null
593
+
594
+ let started = false
595
+ await updateTask(task.id, (current) => {
596
+ const otherRunIds = activeRunIdsFor(current).filter((id) => id !== runId)
597
+ if (mode === 'serial' && otherRunIds.length > 0) return current
598
+ started = true
599
+ const nextRunAt = advanceNextRunAtAtStart
600
+ ? calculateNextRun(current, new Date(startedAt))
601
+ : current.nextRunAt
602
+ const activeRunIds = appendCurrentRunId(current, runId)
603
+ return {
604
+ ...current,
605
+ currentRunId: activeRunIds[activeRunIds.length - 1] || null,
606
+ currentRunIds: activeRunIds,
607
+ lastSessionId: sessionId,
608
+ nextRunAt,
609
+ runs: [{
610
+ id: runId,
611
+ status: 'running',
612
+ trigger,
613
+ inputContent: current.instruction,
614
+ sessionId,
615
+ agentId: executionAgent?.id || task.agentId || null,
616
+ agentLabel: executionAgent?.label || null,
617
+ agentSnapshot,
618
+ warning: agentWarning || undefined,
619
+ scheduledAt,
620
+ startedAt,
621
+ }, ...(current.runs || [])].slice(0, MAX_RUN_HISTORY_PER_TASK),
622
+ }
623
+ })
624
+ if (!started) {
625
+ removeActiveRun(task.id, runId)
626
+ return
627
+ }
538
628
 
539
629
  let settled = false
540
630
 
@@ -550,6 +640,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
550
640
  model: task.model,
551
641
  thinkingLevel: task.thinkingLevel,
552
642
  title: `[定时任务] ${task.title}`,
643
+ agentProfile: executionAgent,
553
644
  })
554
645
 
555
646
  const userMessage = {
@@ -577,7 +668,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
577
668
  await updateTask(task.id, (current) => ({
578
669
  ...current,
579
670
  lastSessionId: sessionId,
580
- runs: (current.runs || []).map((run) => run.id === runId ? { ...run, sessionId } : run),
671
+ runs: (current.runs || []).map((run) => run.id === runId ? { ...run, sessionId, agentId: executionAgent?.id || task.agentId || null, agentLabel: executionAgent?.label || null, agentSnapshot, warning: agentWarning || run.warning } : run),
581
672
  }))
582
673
  onStarted?.({ taskId: task.id, runId, sessionId })
583
674
 
@@ -602,7 +693,7 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
602
693
  const timeout = setTimeout(() => {
603
694
  cleanup(handler, timeout)
604
695
  resolve({ ok: false, aborted: false, error: '执行超时', messages: session.agent.state.messages })
605
- }, 30 * 60 * 1000)
696
+ }, Math.max(1000, Math.min(Number(executionAgent?.maxRuntimeMs || 30 * 60 * 1000), 30 * 60 * 1000)))
606
697
  eventBus?.on('agent_event', handler)
607
698
  })
608
699
 
@@ -622,19 +713,25 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
622
713
  const aiResult = result.ok ? latestAssistantText(result.messages) : ''
623
714
  const latestTask = (await readStore(STORE))[task.id] ?? task
624
715
  const recurring = isRecurringTask(latestTask)
625
- const nextRunAt = calculateNextRun(latestTask, new Date(finishedAt))
626
- const nextStatus = latestTask.status === 'paused'
627
- ? 'paused'
628
- : result.aborted
629
- ? (recurring && nextRunAt ? 'paused' : 'failed')
630
- : result.ok
631
- ? (nextRunAt ? 'enabled' : 'completed')
632
- : (recurring && nextRunAt ? 'enabled' : 'failed')
716
+ removeActiveRun(task.id, runId)
717
+ const remainingRunIds = removeCurrentRunId(latestTask, runId)
718
+ const stillRunning = remainingRunIds.length > 0
719
+ const nextRunAt = stillRunning ? latestTask.nextRunAt : (advanceNextRunAtAtStart ? latestTask.nextRunAt : calculateNextRun(latestTask, new Date(finishedAt)))
720
+ const nextStatus = stillRunning
721
+ ? latestTask.status
722
+ : latestTask.status === 'paused'
723
+ ? 'paused'
724
+ : result.aborted
725
+ ? (recurring && nextRunAt ? 'paused' : 'failed')
726
+ : result.ok
727
+ ? (nextRunAt ? 'enabled' : 'completed')
728
+ : (recurring && nextRunAt ? 'enabled' : 'failed')
633
729
 
634
730
  await updateTask(task.id, (current) => ({
635
731
  ...current,
636
732
  status: nextStatus,
637
- currentRunId: null,
733
+ currentRunId: stillRunning ? remainingRunIds[remainingRunIds.length - 1] : null,
734
+ currentRunIds: remainingRunIds,
638
735
  lastRunAt: finishedAt,
639
736
  nextRunAt: nextRunAt ?? current.nextRunAt,
640
737
  lastSessionId: sessionId,
@@ -646,6 +743,10 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
646
743
  result: result.ok ? (aiResult || `已完成,结果保存在会话 ${sessionId}`) : undefined,
647
744
  errorMessage: result.aborted ? '已暂停执行' : result.error,
648
745
  sessionId,
746
+ agentId: executionAgent?.id || latestTask.agentId || null,
747
+ agentLabel: executionAgent?.label || null,
748
+ agentSnapshot,
749
+ warning: agentWarning || run.warning,
649
750
  finishedAt,
650
751
  durationMs,
651
752
  } : run),
@@ -662,22 +763,32 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
662
763
  } catch (error) {
663
764
  const finishedAt = new Date().toISOString()
664
765
  const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime()
665
- await updateTask(task.id, (current) => ({
666
- ...current,
667
- status: current.status === 'paused' ? 'paused' : (isRecurringTask(current) ? 'enabled' : 'failed'),
668
- currentRunId: null,
669
- lastRunAt: finishedAt,
670
- lastSessionId: sessionId,
671
- nextRunAt: isRecurringTask(current) ? (calculateNextRun(current, new Date(finishedAt)) ?? current.nextRunAt) : current.nextRunAt,
672
- runs: (current.runs || []).map((run) => run.id === runId ? {
673
- ...run,
674
- status: 'failed',
675
- errorMessage: error?.message || String(error),
676
- sessionId,
677
- finishedAt,
678
- durationMs,
679
- } : run),
680
- }))
766
+ removeActiveRun(task.id, runId)
767
+ await updateTask(task.id, (current) => {
768
+ const remainingRunIds = removeCurrentRunId(current, runId)
769
+ const stillRunning = remainingRunIds.length > 0
770
+ return {
771
+ ...current,
772
+ status: stillRunning ? current.status : (current.status === 'paused' ? 'paused' : (isRecurringTask(current) ? 'enabled' : 'failed')),
773
+ currentRunId: stillRunning ? remainingRunIds[remainingRunIds.length - 1] : null,
774
+ currentRunIds: remainingRunIds,
775
+ lastRunAt: finishedAt,
776
+ lastSessionId: sessionId,
777
+ nextRunAt: stillRunning || advanceNextRunAtAtStart ? current.nextRunAt : (isRecurringTask(current) ? (calculateNextRun(current, new Date(finishedAt)) ?? current.nextRunAt) : current.nextRunAt),
778
+ runs: (current.runs || []).map((run) => run.id === runId ? {
779
+ ...run,
780
+ status: 'failed',
781
+ errorMessage: error?.message || String(error),
782
+ sessionId,
783
+ agentId: executionAgent?.id || current.agentId || null,
784
+ agentLabel: executionAgent?.label || null,
785
+ agentSnapshot,
786
+ warning: agentWarning || run.warning,
787
+ finishedAt,
788
+ durationMs,
789
+ } : run),
790
+ }
791
+ })
681
792
  emitScheduledTaskNotification({
682
793
  task,
683
794
  runId,
@@ -687,7 +798,6 @@ async function executeTask(task, trigger = 'schedule', onStarted) {
687
798
  errorMessage: error?.message || String(error),
688
799
  })
689
800
  } finally {
690
- runningTaskIds.delete(task.id)
691
801
  if (!settled) logger.warn(`Scheduled task ${task.id} finished without normal agent_end`)
692
802
  }
693
803
  }
@@ -702,6 +812,7 @@ async function schedulerTick() {
702
812
  for (const task of tasks) {
703
813
  if (task.status !== 'enabled') continue
704
814
  if (!task.nextRunAt || new Date(task.nextRunAt).getTime() > now) continue
815
+ if (executionModeFor(task) === 'serial' && hasActiveTaskRuns(task)) continue
705
816
  executeTask(task, 'schedule').catch((error) => logger.error(`Scheduled task ${task.id} failed:`, error))
706
817
  }
707
818
  } finally {
@@ -721,6 +832,7 @@ export function stopScheduledTaskRunner() {
721
832
  if (!schedulerTimer) return
722
833
  clearInterval(schedulerTimer)
723
834
  schedulerTimer = null
835
+ runningTaskRunIds.clear()
724
836
  }
725
837
 
726
838
  export async function handleScheduledTasksApi(req, res, url) {
@@ -753,6 +865,7 @@ export async function handleScheduledTasksApi(req, res, url) {
753
865
  id: createId(),
754
866
  ...normalized,
755
867
  scheduleRule: normalized.scheduleRule || scheduleRuleFor(normalized),
868
+ executionMode: normalized.executionMode || 'serial',
756
869
  model: body?.model,
757
870
  thinkingLevel: body?.thinkingLevel || (body?.model?.reasoning ? 'medium' : 'off'),
758
871
  projectId: body?.projectId || null,
@@ -788,6 +901,7 @@ export async function handleScheduledTasksApi(req, res, url) {
788
901
  ...current,
789
902
  ...normalized,
790
903
  scheduleRule: normalized.scheduleRule || scheduleRuleFor(normalized),
904
+ executionMode: normalized.executionMode || 'serial',
791
905
  model: body?.model ?? current.model,
792
906
  thinkingLevel: body?.thinkingLevel ?? current.thinkingLevel,
793
907
  projectId: hasProject ? (body.projectId || null) : current.projectId,
@@ -834,7 +948,7 @@ export async function handleScheduledTasksApi(req, res, url) {
834
948
  const data = await readStore(STORE)
835
949
  const task = data[taskId]
836
950
  if (!task) throw requestError('Task not found', 404)
837
- if (runningTaskIds.has(task.id) || task.currentRunId) throw requestError('Task is already running', 409)
951
+ if (executionModeFor(task) === 'serial' && hasActiveTaskRuns(task)) throw requestError('Task is already running', 409)
838
952
  await new Promise((resolve) => {
839
953
  executeTask(task, 'manual', resolve).catch((error) => {
840
954
  logger.error(`Manual scheduled task ${task.id} failed:`, error)
@@ -57,6 +57,14 @@ function sanitizeMessage(message) {
57
57
  return message
58
58
  }
59
59
 
60
+ function sanitizeContextCompaction(compaction) {
61
+ if (!compaction || typeof compaction !== 'object') return null
62
+ return {
63
+ ...compaction,
64
+ summaryMessage: sanitizeMessage(compaction.summaryMessage),
65
+ }
66
+ }
67
+
60
68
  function sanitizeSession(session, record) {
61
69
  const messages = Array.isArray(session?.messages) ? session.messages.map(sanitizeMessage).filter(Boolean) : []
62
70
  return {
@@ -74,6 +82,8 @@ function sanitizeSession(session, record) {
74
82
  tools: Array.isArray(session?.tools) ? session.tools : [],
75
83
  yoloMode: Boolean(session?.yoloMode),
76
84
  messages,
85
+ contextCompaction: sanitizeContextCompaction(session?.contextCompaction),
86
+ contextUsage: null,
77
87
  isStreaming: Boolean(session?.isStreaming || session?.taskStatus === 'running'),
78
88
  taskStatus: session?.taskStatus || session?.status,
79
89
  errorMessage: session?.errorMessage,
@@ -102,6 +112,10 @@ function sanitizeEvent(event) {
102
112
  const next = { ...event }
103
113
  if (next.message) next.message = sanitizeMessage(next.message)
104
114
  if (Array.isArray(next.messages)) next.messages = next.messages.map(sanitizeMessage).filter(Boolean)
115
+ if (next.contextCompaction?.summaryMessage) {
116
+ next.contextCompaction = sanitizeContextCompaction(next.contextCompaction)
117
+ }
118
+ delete next.contextUsage
105
119
  return next
106
120
  }
107
121
 
@@ -56,6 +56,16 @@ export async function handleStorageApi(req, res, url) {
56
56
  values = values.filter((value) => value?.messageCount !== 0)
57
57
  }
58
58
  values.sort((a, b) => {
59
+ if (store === 'sessions-metadata' && indexName === 'lastModified') {
60
+ const leftPinned = getComparable(a, 'pinnedAt')
61
+ const rightPinned = getComparable(b, 'pinnedAt')
62
+ if (leftPinned !== rightPinned) {
63
+ if (leftPinned === undefined || leftPinned === null) return 1
64
+ if (rightPinned === undefined || rightPinned === null) return -1
65
+ return -String(leftPinned).localeCompare(String(rightPinned))
66
+ }
67
+ }
68
+
59
69
  const left = getComparable(a, indexName)
60
70
  const right = getComparable(b, indexName)
61
71
  if (left === right) return 0