Parcourir la source

feat: 工作组机器人按上下文大小和闲置时间自动开始新会话;文件按声明、写文件工具记录归属,并发时不猜(SDK 0.3.1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jerry Liang il y a 1 semaine
Parent
commit
a534cef80d

+ 7 - 0
apps/admin/src/views/BotPlaygroundView.vue

@@ -103,6 +103,7 @@ function toggleStub(botId: string) {
   void setDebug({ stubBotIds: list.includes(botId) ? list.filter(x => x !== botId) : [...list, botId] })
 }
 const interruptBot = (botId: string) => act(() => botClient.teams.interruptBot(teamId.value!, botId))
+const resetSession = (botId: string) => act(() => botClient.teams.resetSession(teamId.value!, botId))
 const removeBot = (botId: string) => act(() => botClient.teams.removeBot(teamId.value!, botId))
 function addBot(e: Event) {
   const el = e.target as HTMLSelectElement
@@ -454,6 +455,12 @@ onBeforeUnmount(() => {
                 </label>
                 <button v-if="b.status !== 'idle' && !readonly" type="button" class="btn-ghost" @click="interruptBot(b.id)"><span class="codicon codicon-debug-stop" />停止</button>
                 <span class="flex-1" />
+                <button
+                  v-if="!readonly && b.sessionId" type="button" class="btn-ghost"
+                  :title="b.role === 'lead' ? '当前任务结束后开始新的会话,之前的工作从任务纪要了解' : '下一轮开始新的会话'" @click="resetSession(b.id)"
+                >
+                  新会话
+                </button>
                 <button v-if="!readonly" type="button" class="btn-ghost" :disabled="t.bots.length <= 1" @click="removeBot(b.id)">移出</button>
               </div>
             </div>

+ 10 - 1
apps/admin/src/views/SettingsView.vue

@@ -155,8 +155,17 @@ const LABELS: Record<(typeof PATH_KEYS)[number], string> = {
               <input id="t-cost" class="field" type="number" min="0.01" step="0.01" :value="draft.teamLimits.maxCostUsdPerJob ?? ''" @change="draft.teamLimits.maxCostUsdPerJob = ($event.target as HTMLInputElement).value ? Number(($event.target as HTMLInputElement).value) : null">
             </div>
             <div><label class="label" for="t-conc">每个工作组同时执行的 Run 数</label><input id="t-conc" v-model.number="draft.teamLimits.maxConcurrentRuns" type="number" min="1" class="field"></div>
+            <div>
+              <label class="label" for="t-ctx">会话上下文上限(token,留空不限)</label>
+              <input id="t-ctx" class="field" type="number" min="10000" step="10000" :value="draft.teamLimits.sessionMaxContextTokens ?? ''" @change="draft.teamLimits.sessionMaxContextTokens = ($event.target as HTMLInputElement).value ? Number(($event.target as HTMLInputElement).value) : null">
+            </div>
+            <div>
+              <label class="label" for="t-idle">闲置多久后开始新会话(小时,留空不限)</label>
+              <input id="t-idle" class="field" type="number" min="1" :value="draft.teamLimits.sessionIdleHours ?? ''" @change="draft.teamLimits.sessionIdleHours = ($event.target as HTMLInputElement).value ? Number(($event.target as HTMLInputElement).value) : null">
+            </div>
           </div>
-          <div class="hint">超出限额时任务自动暂停,由成员选择继续(这一项放宽一倍)或取消。创建工作组时可以调低这些值。</div>
+          <div class="hint">超出任务的限额时任务自动暂停,由成员选择继续(这一项放宽一倍)或取消。创建工作组时可以调低这些值。</div>
+          <div class="hint">机器人的会话上下文超过上限或闲置太久时,下一轮开始新的会话;组长只在两项任务之间换,之前的工作从任务纪要了解。</div>
         </section>
 
         <section class="flex flex-col gap-3">

+ 2 - 0
apps/server/drizzle/0010_session_renewal.sql

@@ -0,0 +1,2 @@
+ALTER TABLE `runs` ADD `context_tokens` integer;--> statement-breakpoint
+ALTER TABLE `team_bot_sessions` ADD `reset_requested` integer DEFAULT false NOT NULL;

+ 2351 - 0
apps/server/drizzle/meta/0010_snapshot.json

@@ -0,0 +1,2351 @@
+{
+  "version": "6",
+  "dialect": "sqlite",
+  "id": "15f0e075-8654-4542-8539-fbbde3abd913",
+  "prevId": "07f8d658-b99a-4e12-a247-7c9d6f9378ab",
+  "tables": {
+    "admin_sessions": {
+      "name": "admin_sessions",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "api_keys": {
+      "name": "api_keys",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_hash": {
+          "name": "key_hash",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_prefix": {
+          "name": "key_prefix",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "allowed_profile_ids": {
+          "name": "allowed_profile_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "default_profile_id": {
+          "name": "default_profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "last_used_at": {
+          "name": "last_used_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "api_keys_key_hash_unique": {
+          "name": "api_keys_key_hash_unique",
+          "columns": [
+            "key_hash"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "audit_logs": {
+      "name": "audit_logs",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "integer",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": true
+        },
+        "ts": {
+          "name": "ts",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "actor": {
+          "name": "actor",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "summary": {
+          "name": "summary",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "detail": {
+          "name": "detail",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "audit_ts_idx": {
+          "name": "audit_ts_idx",
+          "columns": [
+            "ts"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "bot_api_keys": {
+      "name": "bot_api_keys",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_hash": {
+          "name": "key_hash",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_prefix": {
+          "name": "key_prefix",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "allowed_bot_ids": {
+          "name": "allowed_bot_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "last_used_at": {
+          "name": "last_used_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "bot_api_keys_key_hash_unique": {
+          "name": "bot_api_keys_key_hash_unique",
+          "columns": [
+            "key_hash"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "bots": {
+      "name": "bots",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "avatar": {
+          "name": "avatar",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'🤖'"
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "profile_id": {
+          "name": "profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "bots_name_unique": {
+          "name": "bots_name_unique",
+          "columns": [
+            "name"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "custom_skill_versions": {
+      "name": "custom_skill_versions",
+      "columns": {
+        "skill_id": {
+          "name": "skill_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "version": {
+          "name": "version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "reason": {
+          "name": "reason",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "changed": {
+          "name": "changed",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "custom_skill_versions_skill_id_version_pk": {
+          "columns": [
+            "skill_id",
+            "version"
+          ],
+          "name": "custom_skill_versions_skill_id_version_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "custom_skills": {
+      "name": "custom_skills",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "origin": {
+          "name": "origin",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source_id": {
+          "name": "source_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "include_in_all": {
+          "name": "include_in_all",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "published_version": {
+          "name": "published_version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "latest_version": {
+          "name": "latest_version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "working_files": {
+          "name": "working_files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "custom_skills_name_unique": {
+          "name": "custom_skills_name_unique",
+          "columns": [
+            "name"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "events": {
+      "name": "events",
+      "columns": {
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "seq": {
+          "name": "seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "run_id": {
+          "name": "run_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "type": {
+          "name": "type",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "payload": {
+          "name": "payload",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "ts": {
+          "name": "ts",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "events_session_id_seq_pk": {
+          "columns": [
+            "session_id",
+            "seq"
+          ],
+          "name": "events_session_id_seq_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "files": {
+      "name": "files",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "rel_path": {
+          "name": "rel_path",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "origin": {
+          "name": "origin",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "mime": {
+          "name": "mime",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "size": {
+          "name": "size",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "mtime_ms": {
+          "name": "mtime_ms",
+          "type": "real",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "files_session_path_idx": {
+          "name": "files_session_path_idx",
+          "columns": [
+            "session_id",
+            "rel_path"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "interactions": {
+      "name": "interactions",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "run_id": {
+          "name": "run_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "request": {
+          "name": "request",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "response": {
+          "name": "response",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "resolved_at": {
+          "name": "resolved_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "interactions_session_idx": {
+          "name": "interactions_session_idx",
+          "columns": [
+            "session_id",
+            "status"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "jobs": {
+      "name": "jobs",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "no": {
+          "name": "no",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "plan": {
+          "name": "plan",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'running'"
+        },
+        "summary": {
+          "name": "summary",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "blocked_by": {
+          "name": "blocked_by",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "limits": {
+          "name": "limits",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "lead_runs": {
+          "name": "lead_runs",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "run_ids": {
+          "name": "run_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "ended_at": {
+          "name": "ended_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "jobs_team_idx": {
+          "name": "jobs_team_idx",
+          "columns": [
+            "team_id",
+            "no"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "knowledge_roots": {
+      "name": "knowledge_roots",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "path": {
+          "name": "path",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "access": {
+          "name": "access",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'read'"
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "mcp_servers": {
+      "name": "mcp_servers",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "transport": {
+          "name": "transport",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "enabled": {
+          "name": "enabled",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": true
+        },
+        "url": {
+          "name": "url",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "headers": {
+          "name": "headers",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "command": {
+          "name": "command",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "args": {
+          "name": "args",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "env": {
+          "name": "env",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "allow_network": {
+          "name": "allow_network",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "read_paths": {
+          "name": "read_paths",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "write_paths": {
+          "name": "write_paths",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "tool_policies": {
+          "name": "tool_policies",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{}'"
+        },
+        "tools_cache": {
+          "name": "tools_cache",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "tools_updated_at": {
+          "name": "tools_updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "mcp_servers_name_unique": {
+          "name": "mcp_servers_name_unique",
+          "columns": [
+            "name"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "profile_versions": {
+      "name": "profile_versions",
+      "columns": {
+        "profile_id": {
+          "name": "profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "version": {
+          "name": "version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "snapshot": {
+          "name": "snapshot",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "profile_versions_profile_id_version_pk": {
+          "columns": [
+            "profile_id",
+            "version"
+          ],
+          "name": "profile_versions_profile_id_version_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "profiles": {
+      "name": "profiles",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "provider": {
+          "name": "provider",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "system_prompt": {
+          "name": "system_prompt",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "permission_mode": {
+          "name": "permission_mode",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'ask'"
+        },
+        "allowed_tools": {
+          "name": "allowed_tools",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "disallowed_tools": {
+          "name": "disallowed_tools",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "skills": {
+          "name": "skills",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'\"all\"'"
+        },
+        "file_access": {
+          "name": "file_access",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "knowledge_root_ids": {
+          "name": "knowledge_root_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "mcp_server_ids": {
+          "name": "mcp_server_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "is_default": {
+          "name": "is_default",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "builtin": {
+          "name": "builtin",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "builtin_key": {
+          "name": "builtin_key",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "version": {
+          "name": "version",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 1
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "runs": {
+      "name": "runs",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "started_at": {
+          "name": "started_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "ended_at": {
+          "name": "ended_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_read_tokens": {
+          "name": "cache_read_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_write_tokens": {
+          "name": "cache_write_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "real",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "context_tokens": {
+          "name": "context_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "error": {
+          "name": "error",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "runs_session_idx": {
+          "name": "runs_session_idx",
+          "columns": [
+            "session_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "sessions": {
+      "name": "sessions",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source": {
+          "name": "source",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "profile_id": {
+          "name": "profile_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "permission_mode": {
+          "name": "permission_mode",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "system_prompt_append": {
+          "name": "system_prompt_append",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "provider_session_id": {
+          "name": "provider_session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "workspace_dir": {
+          "name": "workspace_dir",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "last_seq": {
+          "name": "last_seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "archived": {
+          "name": "archived",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "sessions_source_idx": {
+          "name": "sessions_source_idx",
+          "columns": [
+            "source",
+            "updated_at"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "settings": {
+      "name": "settings",
+      "columns": {
+        "key": {
+          "name": "key",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "value": {
+          "name": "value",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "skill_sources": {
+      "name": "skill_sources",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "kind": {
+          "name": "kind",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "path": {
+          "name": "path",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "sync_mode": {
+          "name": "sync_mode",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'manual'"
+        },
+        "interval_min": {
+          "name": "interval_min",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 10
+        },
+        "last_sync_at": {
+          "name": "last_sync_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "last_sync_digest": {
+          "name": "last_sync_digest",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'never'"
+        },
+        "last_error": {
+          "name": "last_error",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "tasks": {
+      "name": "tasks",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "job_id": {
+          "name": "job_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "no": {
+          "name": "no",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "bot_id": {
+          "name": "bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "instructions": {
+          "name": "instructions",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "depends_on": {
+          "name": "depends_on",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "result": {
+          "name": "result",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "run_id": {
+          "name": "run_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "started_at": {
+          "name": "started_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "ended_at": {
+          "name": "ended_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "tasks_team_idx": {
+          "name": "tasks_team_idx",
+          "columns": [
+            "team_id",
+            "no"
+          ],
+          "isUnique": false
+        },
+        "tasks_job_idx": {
+          "name": "tasks_job_idx",
+          "columns": [
+            "job_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_bot_sessions": {
+      "name": "team_bot_sessions",
+      "columns": {
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "bot_id": {
+          "name": "bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "session_id": {
+          "name": "session_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "last_read_seq": {
+          "name": "last_read_seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "reset_requested": {
+          "name": "reset_requested",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": false
+        }
+      },
+      "indexes": {
+        "team_bot_sessions_session_id_unique": {
+          "name": "team_bot_sessions_session_id_unique",
+          "columns": [
+            "session_id"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "team_bot_sessions_team_id_bot_id_pk": {
+          "columns": [
+            "team_id",
+            "bot_id"
+          ],
+          "name": "team_bot_sessions_team_id_bot_id_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_bots": {
+      "name": "team_bots",
+      "columns": {
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "bot_id": {
+          "name": "bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "role": {
+          "name": "role",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "joined_at": {
+          "name": "joined_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "team_bots_bot_idx": {
+          "name": "team_bots_bot_idx",
+          "columns": [
+            "bot_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "team_bots_team_id_bot_id_pk": {
+          "columns": [
+            "team_id",
+            "bot_id"
+          ],
+          "name": "team_bots_team_id_bot_id_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_cases": {
+      "name": "team_cases",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "lead_bot_id": {
+          "name": "lead_bot_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "worker_bot_ids": {
+          "name": "worker_bot_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "stub_bot_ids": {
+          "name": "stub_bot_ids",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "limits": {
+          "name": "limits",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "human_name": {
+          "name": "human_name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "text": {
+          "name": "text",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "mentions": {
+          "name": "mentions",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "files": {
+          "name": "files",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'[]'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "team_events": {
+      "name": "team_events",
+      "columns": {
+        "team_id": {
+          "name": "team_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "seq": {
+          "name": "seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "type": {
+          "name": "type",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "payload": {
+          "name": "payload",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "ts": {
+          "name": "ts",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "team_events_team_id_seq_pk": {
+          "columns": [
+            "team_id",
+            "seq"
+          ],
+          "name": "team_events_team_id_seq_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "teams": {
+      "name": "teams",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source": {
+          "name": "source",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "''"
+        },
+        "human": {
+          "name": "human",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "status": {
+          "name": "status",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'active'"
+        },
+        "workspace_dir": {
+          "name": "workspace_dir",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "limits": {
+          "name": "limits",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "debug": {
+          "name": "debug",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "'{\"stepMode\":false,\"stubBotIds\":[]}'"
+        },
+        "last_seq": {
+          "name": "last_seq",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "case_id": {
+          "name": "case_id",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(strftime('%Y-%m-%dT%H:%M:%fZ','now'))"
+        }
+      },
+      "indexes": {
+        "teams_source_idx": {
+          "name": "teams_source_idx",
+          "columns": [
+            "source",
+            "updated_at"
+          ],
+          "isUnique": false
+        },
+        "teams_case_idx": {
+          "name": "teams_case_idx",
+          "columns": [
+            "case_id"
+          ],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "usage_daily": {
+      "name": "usage_daily",
+      "columns": {
+        "date": {
+          "name": "date",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source": {
+          "name": "source",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "runs": {
+          "name": "runs",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "failed": {
+          "name": "failed",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "interrupted": {
+          "name": "interrupted",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "cache_read_tokens": {
+          "name": "cache_read_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "cache_write_tokens": {
+          "name": "cache_write_tokens",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "real",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        },
+        "duration_ms": {
+          "name": "duration_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": 0
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "usage_daily_date_source_provider_model_pk": {
+          "columns": [
+            "date",
+            "source",
+            "provider",
+            "model"
+          ],
+          "name": "usage_daily_date_source_provider_model_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    }
+  },
+  "views": {},
+  "enums": {},
+  "_meta": {
+    "schemas": {},
+    "tables": {},
+    "columns": {}
+  },
+  "internal": {
+    "indexes": {}
+  }
+}

+ 7 - 0
apps/server/drizzle/meta/_journal.json

@@ -71,6 +71,13 @@
       "when": 1793084140773,
       "tag": "0009_lead_profile",
       "breakpoints": true
+    },
+    {
+      "idx": 10,
+      "version": "6",
+      "when": 1793086786895,
+      "tag": "0010_session_renewal",
+      "breakpoints": true
     }
   ]
 }

+ 5 - 0
apps/server/src/api-reference/bot-reference.ts

@@ -139,6 +139,11 @@ export const BOT_GROUPS: ApiEndpointGroup[] = [
         method: 'POST', path: team('/bots/:botId/interrupt'), summary: '中断机器人的本轮工作',
         response: { status: 202, type: '{ ok: true }' },
       },
+      {
+        method: 'POST', path: team('/bots/:botId/reset-session'), summary: '让机器人开始新的会话',
+        desc: '下一轮不再续接之前的对话(组长在当前任务结束后)。会话的上下文超过 `limits.sessionMaxContextTokens` 或闲置超过 `limits.sessionIdleHours` 时也会自动开始新的会话;组长可以从共享目录 `.team/jobs/` 下的任务纪要了解之前的工作。',
+        response: { status: 200, type: '{ ok: true }' },
+      },
     ],
   },
   {

+ 4 - 0
apps/server/src/db/schema.ts

@@ -87,6 +87,8 @@ export const runs = sqliteTable('runs', {
   cacheReadTokens: integer('cache_read_tokens'),
   cacheWriteTokens: integer('cache_write_tokens'),
   costUsd: real('cost_usd'),
+  /** 最后一次模型调用的上下文大小(判断工作组机器人的会话是否过长) */
+  contextTokens: integer('context_tokens'),
   error: text('error'),
   createdAt: createdAt(),
 }, t => [index('runs_session_idx').on(t.sessionId)])
@@ -310,6 +312,8 @@ export const teamBotSessions = sqliteTable('team_bot_sessions', {
   sessionId: text('session_id').notNull().unique(),
   /** 已交给这个机器人的工作组事件 seq */
   lastReadSeq: integer('last_read_seq').notNull().default(0),
+  /** 成员要求开始新的会话:下一轮不再续接 */
+  resetRequested: integer('reset_requested', { mode: 'boolean' }).notNull().default(false),
 }, t => [primaryKey({ columns: [t.teamId, t.botId] })])
 
 export const teamEvents = sqliteTable('team_events', {

+ 7 - 0
apps/server/src/providers/claude/adapter.ts

@@ -331,6 +331,8 @@ interface OpenBlock { kind: 'text' | 'thinking'; id: string; text: string }
 /** 把 SDK 消息流映射为统一事件;收到 result 时返回 RunResult */
 class MessageMapper {
   private blocks = new Map<number, OpenBlock>()
+  /** 主线程最后一次模型调用的上下文大小 */
+  private lastContext = 0
   private skillNames: Map<string, string>
 
   constructor(private ctx: RunContext, private deniedToolIds: Set<string>) {
@@ -348,6 +350,10 @@ class MessageMapper {
       case 'assistant':
         if (m.error) this.ctx.log.warn({ error: m.error }, 'claude assistant error')
         if (m.parent_tool_use_id !== null) return null
+        if (m.message.usage) {
+          const u = m.message.usage
+          this.lastContext = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.output_tokens ?? 0)
+        }
         for (const block of m.message.content) {
           if (block.type === 'tool_use') {
             const input = (block.input ?? {}) as Record<string, unknown>
@@ -379,6 +385,7 @@ class MessageMapper {
           cacheReadTokens: u.cache_read_input_tokens ?? 0,
           cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
           costUsd: m.total_cost_usd,
+          ...(this.lastContext ? { contextTokens: this.lastContext } : {}),
         }
         if (m.subtype !== 'success' || m.is_error) {
           const detail = m.subtype === 'success' ? m.result : m.errors.join('; ') || m.subtype

+ 4 - 0
apps/server/src/providers/codex/adapter.ts

@@ -324,6 +324,8 @@ class ThreadRun {
   private denied = new Set<string>()
   private usageBase: UsagePayload | null = null
   private usageLatest: UsagePayload | null = null
+  /** 最后一次模型调用的上下文大小 */
+  private lastContext = 0
 
   constructor(private ctx: RunContext, private threadId: string) {
     this.done = new Promise((res, rej) => {
@@ -380,6 +382,7 @@ class ThreadRun {
           }
         }
         this.usageLatest = toUsage(tokenUsage.total)
+        this.lastContext = tokenUsage.last.inputTokens + tokenUsage.last.outputTokens
         break
       }
       case 'turn/completed': {
@@ -409,6 +412,7 @@ class ThreadRun {
     return {
       inputTokens: l.inputTokens - b.inputTokens, outputTokens: l.outputTokens - b.outputTokens,
       cacheReadTokens: l.cacheReadTokens - b.cacheReadTokens, cacheWriteTokens: l.cacheWriteTokens - b.cacheWriteTokens,
+      ...(this.lastContext ? { contextTokens: this.lastContext } : {}),
     }
   }
 

+ 1 - 0
apps/server/src/services/runs.ts

@@ -265,6 +265,7 @@ export class RunManager {
         deps.db.update(runs).set({
           inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens,
           cacheReadTokens: result.usage.cacheReadTokens, cacheWriteTokens: result.usage.cacheWriteTokens, costUsd: result.usage.costUsd ?? null,
+          contextTokens: result.usage.contextTokens ?? null,
         }).where(eq(runs.id, runId)).run()
       }
       this.finish(a, 'completed', { durationMs: Date.now() - startedAt })

+ 3 - 1
apps/server/src/services/settings.ts

@@ -31,7 +31,7 @@ export function defaultSettings(): AppSettings {
       workspaceMaxMB: 2048,
     },
     retention: { workspaceDays: 30 },
-    teamLimits: { maxTasksPerJob: 20, maxLeadRunsPerJob: 30, maxCostUsdPerJob: null, maxMinutesPerJob: 120, maxConcurrentRuns: 4 },
+    teamLimits: { maxTasksPerJob: 20, maxLeadRunsPerJob: 30, maxCostUsdPerJob: null, maxMinutesPerJob: 120, maxConcurrentRuns: 4, sessionMaxContextTokens: 100_000, sessionIdleHours: 24 },
     providers: {
       claude: {
         enabled: true,
@@ -59,6 +59,8 @@ export const teamLimitsSchema = z.object({
   maxCostUsdPerJob: z.number().min(0.01).max(10000).nullable(),
   maxMinutesPerJob: z.number().int().min(1).max(7 * 24 * 60),
   maxConcurrentRuns: z.number().int().min(1).max(32),
+  sessionMaxContextTokens: z.number().int().min(10_000).max(2_000_000).nullable(),
+  sessionIdleHours: z.number().min(1).max(24 * 365).nullable(),
 })
 
 const settingsSchema: z.ZodType<AppSettings> = z.object({

+ 30 - 0
apps/server/src/teams/attribution.ts

@@ -0,0 +1,30 @@
+// 文件归属(docs/design/13-bot-teams.md §19):共享目录中改动的文件算作哪个机器人的产出。
+// 声明的文件(task_complete / job_complete 的 files)为准;写文件工具的记录可以准确归属;
+// 只能靠扫描发现的文件(命令、脚本生成)只有在这一轮没有其他机器人同时工作时才算给它
+import path from 'node:path'
+
+const PATH_FIELDS: Record<string, string> = { Write: 'file_path', Edit: 'file_path', MultiEdit: 'file_path', NotebookEdit: 'notebook_path' }
+
+/** 工具开始时就能确定的写入路径:Claude 的写文件工具、Codex 的文件修改、recalc_workbook(写回原文件) */
+export function plannedWrites(name: string, input: unknown): string[] {
+  const a = (input ?? {}) as Record<string, unknown>
+  const field = PATH_FIELDS[name]
+  if (field && typeof a[field] === 'string') return [a[field] as string]
+  // Codex 的文件修改映射为 Edit,输入是 changes 列表
+  if (name === 'Edit' && Array.isArray(a.changes)) return a.changes.flatMap(c => (typeof c?.path === 'string' ? [c.path as string] : []))
+  if (name.endsWith('__recalc_workbook') && typeof a.input === 'string') return [a.input]
+  return []
+}
+
+/** 工具成功后才知道的写入路径:convert_document 的结果是"已生成:<路径>" */
+export function reportedWrites(name: string, output: string): string[] {
+  if (!name.endsWith('__convert_document')) return []
+  const m = /已生成:(.+)$/m.exec(output)
+  return m ? [m[1]!.trim()] : []
+}
+
+/** 转成共享目录内的相对路径(与文件索引一致);目录外的返回 null */
+export function relInWorkspace(workspaceDir: string, p: string): string | null {
+  const rel = path.relative(workspaceDir, path.resolve(workspaceDir, p))
+  return !rel || rel.startsWith('..') || path.isAbsolute(rel) ? null : rel.split(path.sep).join('/')
+}

+ 1 - 1
apps/server/src/teams/prompt.ts

@@ -49,7 +49,7 @@ ${cards.length ? cards.map(cardText).join('\n\n') : '(还没有组员。可以
 
 const WORKER_RULES = `# 子任务
 组长会把子任务分派给你,输入以【子任务 #编号】开头。
-- 完成后调用 task_complete(result, files) 汇报:result 写清楚做了什么、结论是什么,files 是交付文件(outputs/ 下的路径)。
+- 完成后调用 task_complete(result, files) 汇报:result 写清楚做了什么、结论是什么,files 是交付文件(outputs/ 下的路径)。用命令或脚本生成的文件一定要列在 files 中:几个机器人同时工作时,系统无法判断这些文件是谁生成的。
 - 无法完成时调用 task_fail(reason) 说明原因,不要编造结果。
 - 子任务的结果只交给组长;有疑问时在结果中说明,由组长决定。
 成员也可能直接 @ 你做单项工作,这时直接回复成员即可,不需要调用 task_complete。`

+ 4 - 0
apps/server/src/teams/routes.ts

@@ -74,6 +74,10 @@ export default async function botRoutes(f: FastifyInstance, { app }: { app: App
     const { botId } = parse(z.object({ botId: z.string().min(1) }), req.body)
     return app.teams.detail(app.teams.addBot(req.params.id, source(req), botId))
   })
+  f.post<BotParams>('/teams/:id/bots/:botId/reset-session', async req => {
+    app.teams.resetSession(req.params.id, source(req), req.params.botId)
+    return { ok: true }
+  })
   f.delete<BotParams>('/teams/:id/bots/:botId', async req => app.teams.detail(app.teams.removeBot(req.params.id, source(req), req.params.botId)))
   f.post<BotParams>('/teams/:id/bots/:botId/interrupt', async (req, reply) => {
     app.teams.interrupt(req.params.id, source(req), req.params.botId)

+ 82 - 5
apps/server/src/teams/service.ts

@@ -25,6 +25,7 @@ import type { RunAttachment, RunManager } from '../services/runs'
 import { isTeamSource, TEAM_SOURCE_PREFIX, type SessionRow, type SessionService } from '../services/sessions'
 import { teamLimitsSchema, type SettingsService } from '../services/settings'
 import type { BotKeyService, BotService } from './bots'
+import { plannedWrites, relInWorkspace, reportedWrites } from './attribution'
 import { cardText, type CardService } from './cards'
 import { TeamEventStore } from './events'
 import { JobStore, LIMIT_LABELS, OPEN_TASK, type JobRow, type TaskRow } from './jobs'
@@ -73,9 +74,14 @@ interface BotRuntime {
   pending: boolean
   /** 正在准备开始一轮(同步文件索引中),这期间到达的消息同样记为 pending */
   starting: boolean
-  /** 本轮最后一段回复与改动过的文件(用于回帖) */
+  /** 本轮最后一段回复与扫描到的改动文件(用于回帖) */
   lastText: string
   changedPaths: Set<string>
+  /** 本轮写文件工具确实写过的文件,以及进行中的写文件工具(toolCallId → 路径) */
+  writtenPaths: Set<string>
+  pendingWrites: Map<string, { name: string; paths: string[] }>
+  /** 本轮有其他机器人同时工作:扫描到的改动无法确定是谁的,只认写文件工具的记录 */
+  overlapped: boolean
   /** 本轮执行的子任务,以及组员调用 task_complete / task_fail 报告的结果 */
   taskId: string | null
   taskOutcome: { status: 'completed' | 'failed'; result: string; paths: string[] } | null
@@ -365,6 +371,36 @@ export class TeamService {
     this.interruptBot(id, botId)
   }
 
+  /** 要求机器人下一轮开始新的会话(组长在当前任务结束后) */
+  resetSession(id: string, source: string, botId: string) {
+    this.getRow(id, source)
+    const member = this.memberRows(id).find(m => m.botId === botId)
+    if (!member) throw notFound('工作组中的机器人')
+    this.deps.db.update(teamBotSessions).set({ resetRequested: true })
+      .where(and(eq(teamBotSessions.teamId, id), eq(teamBotSessions.botId, botId))).run()
+    const later = member.role === 'lead' && this.jobs.active(id) ? '当前任务结束后' : '下一轮'
+    this.system(id, `${this.botName(botId)} 将在${later}开始新的会话`)
+  }
+
+  /**
+   * 是否开始新的会话:成员要求、上一轮的上下文超过限额、闲置太久。
+   * 组长在任务进行中不换(任务的来龙去脉在会话里),等任务结束;组员每轮都可以换(子任务的输入是完整的)
+   */
+  private renewReason(team: TeamRow, role: BotDto['kind'], sessionId: string, requested: boolean): string | null {
+    if (role === 'lead' && this.jobs.active(team.id)) return null
+    if (requested) return '成员要求开始新的会话'
+    const last = this.deps.db.select({ contextTokens: runs.contextTokens, endedAt: runs.endedAt, createdAt: runs.createdAt })
+      .from(runs).where(eq(runs.sessionId, sessionId)).orderBy(desc(runs.createdAt)).limit(1).get()
+    if (!last) return null
+    const limits = this.limitsOf(team)
+    if (limits.sessionMaxContextTokens !== null && (last.contextTokens ?? 0) > limits.sessionMaxContextTokens) {
+      return `上一个会话的上下文已有 ${Math.round(last.contextTokens! / 1000)}k token`
+    }
+    const idleHours = (Date.now() - Date.parse(last.endedAt ?? last.createdAt)) / 3_600_000
+    if (limits.sessionIdleHours !== null && idleHours > limits.sessionIdleHours) return `已闲置 ${Math.floor(idleHours)} 小时`
+    return null
+  }
+
   pendingInteractions(teamId: string): TeamInteractionDto[] {
     return this.deps.db.select().from(teamBotSessions).where(eq(teamBotSessions.teamId, teamId)).all()
       .flatMap(l => this.deps.interactions.pendingFor(l.sessionId).map(request => ({ botId: l.botId, sessionId: l.sessionId, request })))
@@ -697,10 +733,17 @@ export class TeamService {
       session = null
       this.system(team.id, `${bot.name} 的引擎已更换,开始新的会话(之前的对话记忆不再保留)`, 'warning')
     }
+    // 会话过长、闲置太久或成员要求时开始新的会话(§19)
+    const renew = session && link ? this.renewReason(team, member.role, session.id, link.resetRequested) : null
+    if (session && renew) {
+      this.sessionIndex.set(session.id, null)
+      session = null
+      this.system(team.id, `${bot.name} 开始新的会话:${renew}${member.role === 'lead' ? '。之前的任务纪要在 .team/jobs/ 下' : ''}`)
+    }
     if (!session) {
       session = this.deps.sessions.createForTeam({ teamId: team.id, profile, workspaceDir: team.workspaceDir, title, systemPromptAppend: prompt })
       this.deps.db.insert(teamBotSessions).values({ teamId: team.id, botId, sessionId: session.id, lastReadSeq: lastRead })
-        .onConflictDoUpdate({ target: [teamBotSessions.teamId, teamBotSessions.botId], set: { sessionId: session.id } }).run()
+        .onConflictDoUpdate({ target: [teamBotSessions.teamId, teamBotSessions.botId], set: { sessionId: session.id, resetRequested: false } }).run()
       this.sessionIndex.set(session.id, { teamId: team.id, botId })
       this.changed(team.id)
     } else {
@@ -757,12 +800,29 @@ export class TeamService {
     const rt = this.rt(ref.teamId, ref.botId)
     switch (e.type) {
       case 'run.queued':
-        Object.assign(rt, { lastText: '', changedPaths: new Set<string>(), posted: false })
+        Object.assign(rt, { lastText: '', changedPaths: new Set<string>(), writtenPaths: new Set<string>(), pendingWrites: new Map(), overlapped: false, posted: false })
         this.setBotStatus(ref, e.sessionId, 'queued', e.runId)
         break
       case 'run.started':
+        this.markOverlap(ref)
         this.setBotStatus(ref, e.sessionId, 'working', e.runId)
         break
+      case 'tool.started': {
+        const paths = plannedWrites(e.payload.name, e.payload.input)
+        rt.pendingWrites.set(e.payload.toolCallId, { name: e.payload.name, paths })
+        break
+      }
+      case 'tool.completed': {
+        const call = rt.pendingWrites.get(e.payload.toolCallId)
+        rt.pendingWrites.delete(e.payload.toolCallId)
+        const dir = call && e.payload.status === 'ok' ? this.findRow(ref.teamId)?.workspaceDir : undefined
+        if (!call || !dir) break
+        for (const p of [...call.paths, ...reportedWrites(call.name, e.payload.output)]) {
+          const rel = relInWorkspace(dir, p)
+          if (rel) rt.writtenPaths.add(rel)
+        }
+        break
+      }
       case 'message.completed':
         if (e.payload.text.trim()) rt.lastText = e.payload.text
         break
@@ -785,13 +845,27 @@ export class TeamService {
     }
   }
 
+  /** 开始一轮时,与正在工作的其他机器人互相标记为"同时工作" */
+  private markOverlap(ref: { teamId: string; botId: string }) {
+    const self = this.rt(ref.teamId, ref.botId)
+    for (const [key, other] of this.runtime) {
+      if (!key.startsWith(`${ref.teamId}/`) || other === self) continue
+      if (other.status === 'working' || other.status === 'waiting_input') {
+        other.overlapped = true
+        self.overlapped = true
+      }
+    }
+  }
+
   private async onRunEnd(ref: { teamId: string; botId: string }, e: AgentEvent) {
     const team = this.findRow(ref.teamId)
     if (!team) return
     const rt = this.rt(ref.teamId, ref.botId)
     const name = this.botName(ref.botId)
     await this.scanFiles(team)
-    const outputs = this.deps.files.list(team.id).filter(f => rt.changedPaths.has(f.path) && f.path.startsWith('outputs/'))
+    // 写文件工具的记录准确;扫描到的改动只在没有其他机器人同时工作时算给它(§19)
+    const owned = (p: string) => rt.writtenPaths.has(p) || (!rt.overlapped && rt.changedPaths.has(p))
+    const outputs = this.deps.files.list(team.id).filter(f => owned(f.path) && f.path.startsWith('outputs/'))
     const files: TeamFileRef[] = outputs.map(f => ({ fileId: f.id, path: f.path, mime: f.mime, size: f.size }))
 
     if (rt.taskId) {
@@ -1195,7 +1269,10 @@ export class TeamService {
   private rt(teamId: string, botId: string): BotRuntime {
     let r = this.runtime.get(key(teamId, botId))
     if (!r) {
-      r = { status: 'idle', runId: null, pending: false, starting: false, lastText: '', changedPaths: new Set(), taskId: null, taskOutcome: null, posted: false }
+      r = {
+        status: 'idle', runId: null, pending: false, starting: false, lastText: '', changedPaths: new Set(),
+        writtenPaths: new Set(), pendingWrites: new Map(), overlapped: false, taskId: null, taskOutcome: null, posted: false,
+      }
       this.runtime.set(key(teamId, botId), r)
     }
     return r

+ 2 - 1
apps/server/test/codex.test.ts

@@ -50,7 +50,8 @@ describe('CodexProvider(模拟 app-server)', () => {
     expect(t.interactions[0]).toMatchObject({ kind: 'approval', tool: 'Bash', detail: { command: 'ls -la' } })
     expect(t.events.find(e => e.type === 'tool.completed')!.payload).toMatchObject({ status: 'ok', output: 'a.csv' })
     // 本次用量 = 最新累计 - (首次累计 - 首次增量) = 220-100, 45-20
-    expect(r.usage).toEqual({ inputTokens: 120, outputTokens: 25, cacheReadTokens: 0, cacheWriteTokens: 0 })
+    // 上下文大小取最后一次调用的输入 + 输出
+    expect(r.usage).toEqual({ inputTokens: 120, outputTokens: 25, cacheReadTokens: 0, cacheWriteTokens: 0, contextTokens: 85 })
 
     const cfg = JSON.parse((t.events.find(e => e.type === 'message.completed' && (e.payload as { messageId: string }).messageId === 'cfg')!.payload as { text: string }).text)
     const fsTable = cfg.permissions.agent_server.filesystem

+ 81 - 3
apps/server/test/team-jobs.test.ts

@@ -15,11 +15,13 @@ type Script = (input: string, call: Call, ctx: RunContext) => Promise<string>
 class ScriptedProvider implements AgentProvider {
   readonly id = 'claude' as const
   scripts: Record<string, Script> = {}
-  inputs: { bot: string; text: string; prompt: string }[] = []
+  inputs: { bot: string; text: string; prompt: string; sessionId: string }[] = []
+  /** 报告的上下文大小(判断是否开始新会话) */
+  context = 0
   async run(ctx: RunContext) {
     ctx.setProviderSessionId(`fake-${ctx.sessionId}`)
     const bot = app.sessions.getRow(ctx.sessionId).title.split(' · ').at(-1)!
-    this.inputs.push({ bot, text: ctx.input.text, prompt: ctx.systemPromptAppend })
+    this.inputs.push({ bot, text: ctx.input.text, prompt: ctx.systemPromptAppend, sessionId: ctx.sessionId })
     const gctx = app.gatewayTokens.get(ctx.gateway.token)!
     const call: Call = async (tool, args) => {
       const t = gctx.extraTools?.find(x => x.name === tool)
@@ -29,7 +31,7 @@ class ScriptedProvider implements AgentProvider {
     }
     const reply = await (this.scripts[bot] ?? (async () => `${bot} 收到`))(ctx.input.text, call, ctx)
     ctx.emit('message.completed', { messageId: `m${this.inputs.length}`, text: reply })
-    return { usage: { inputTokens: 10, outputTokens: 5, cacheReadTokens: 0, cacheWriteTokens: 0, costUsd: 0.01 } }
+    return { usage: { inputTokens: 10, outputTokens: 5, cacheReadTokens: 0, cacheWriteTokens: 0, costUsd: 0.01, ...(this.context ? { contextTokens: this.context } : {}) } }
   }
   async status(): Promise<ProviderStatusDto> {
     return { id: 'claude', enabled: true, available: true, loggedIn: true, account: 'test', cliPath: '', version: 'x', models: ['m'], defaultModel: 'm', message: null }
@@ -422,6 +424,82 @@ describe('Bot Playground 的调试功能', () => {
   })
 })
 
+describe('文件归属', () => {
+  it('两个机器人同时工作时,脚本生成的文件不算给任何一方;写文件工具写的文件准确归属', async () => {
+    const team = (await server.inject({ method: 'POST', url: '/bot/v1/teams', headers: bk(), payload: { human, workerBotIds: [ids['数据员'], ids['文档员']] } })).json() as TeamDetailDto
+    let bStarted!: () => void
+    const started = new Promise<void>(r => (bStarted = r))
+    let aDone!: () => void
+    const aFinished = new Promise<void>(r => (aDone = r))
+    fake.scripts['数据员'] = async (_input, _call, ctx) => {
+      await started
+      fs.mkdirSync(path.join(ctx.workspaceDir, 'outputs'), { recursive: true })
+      fs.writeFileSync(path.join(ctx.workspaceDir, 'outputs/a-script.txt'), 'a')
+      ctx.emit('tool.started', { toolCallId: 'w1', name: 'Write', title: 'a-tool', input: { file_path: path.join(ctx.workspaceDir, 'outputs/a-tool.txt') } })
+      fs.writeFileSync(path.join(ctx.workspaceDir, 'outputs/a-tool.txt'), 'a')
+      ctx.emit('tool.completed', { toolCallId: 'w1', status: 'ok', output: '', truncated: false })
+      aDone()
+      return 'A 完成'
+    }
+    fake.scripts['文档员'] = async (_input, _call, ctx) => {
+      bStarted()
+      fs.mkdirSync(path.join(ctx.workspaceDir, 'outputs'), { recursive: true })
+      fs.writeFileSync(path.join(ctx.workspaceDir, 'outputs/b-script.txt'), 'b')
+      await aFinished
+      return 'B 完成'
+    }
+    await server.inject({ method: 'POST', url: `/bot/v1/teams/${team.id}/messages`, headers: bk(), payload: { text: '各自干活', mentions: [ids['数据员'], ids['文档员']] } })
+    await waitFor(() => idle(team.id) && messages(team.id).filter(m => m.sender.kind === 'bot').length === 2)
+    const reply = (text: string) => messages(team.id).find(m => m.text === text)!.files.map(f => f.path)
+    expect(reply('A 完成')).toEqual(['outputs/a-tool.txt'])
+    expect(reply('B 完成')).toEqual([])
+    // 文件都在共享目录中
+    const all = app.teams.events.list(team.id, 0, 5000, ['file.changed']).length
+    expect(all).toBeGreaterThanOrEqual(3)
+  })
+})
+
+describe('开始新的会话', () => {
+  it('组员的上下文超过限额时下一轮开始新会话;组长在任务进行中不换,任务结束后再换;成员可以要求开始新会话', async () => {
+    const team = (await server.inject({
+      method: 'POST', url: '/bot/v1/teams', headers: bk(),
+      payload: { human, leadBotId: ids['组长'], workerBotIds: [ids['数据员']], limits: { sessionMaxContextTokens: 10_000 } },
+    })).json() as TeamDetailDto
+    const sessionsOf = (bot: string) => [...new Set(fake.inputs.filter(i => i.bot === bot && app.sessions.getRow(i.sessionId).source === `team:${team.id}`).map(i => i.sessionId))]
+    const say = async (text: string, mentions?: string[]) => {
+      const before = fake.inputs.length
+      await server.inject({ method: 'POST', url: `/bot/v1/teams/${team.id}/messages`, headers: bk(), payload: { text, ...(mentions ? { mentions } : {}) } })
+      await waitFor(() => fake.inputs.length > before && idle(team.id))
+    }
+    fake.scripts['数据员'] = async () => 'ok'
+    fake.scripts['组长'] = async (input, call) => {
+      if (input.includes('开始任务')) await call('job_start', { title: '长任务', plan: '' })
+      if (input.includes('结束任务')) await call('job_complete', { summary: '完成', notes: '' })
+      return 'ok'
+    }
+    fake.context = 20_000
+    await say('一', [ids['数据员']!])
+    await say('二', [ids['数据员']!])
+    expect(sessionsOf('数据员')).toHaveLength(2)
+    expect(messages(team.id).some(m => m.sender.kind === 'system' && m.text.includes('数据员 开始新的会话:上一个会话的上下文已有 20k token'))).toBe(true)
+
+    await say('开始任务')
+    await say('补充说明')
+    await say('结束任务')
+    // 第一轮之后上下文已超限,但任务进行中不换
+    expect(sessionsOf('组长')).toHaveLength(1)
+    await say('新的问题')
+    expect(sessionsOf('组长')).toHaveLength(2)
+
+    fake.context = 0
+    const r = await server.inject({ method: 'POST', url: `/bot/v1/teams/${team.id}/bots/${ids['数据员']}/reset-session`, headers: bk() })
+    expect(r.statusCode).toBe(200)
+    await say('三', [ids['数据员']!])
+    expect(sessionsOf('数据员')).toHaveLength(3)
+    expect(messages(team.id).some(m => m.text.includes('成员要求开始新的会话'))).toBe(true)
+  })
+})
+
 describe('复制到 Playground 与回归用例', () => {
   it('从平台的工作组复制、保存用例(含附件),重放后列出结果', async () => {
     const team = (await server.inject({ method: 'POST', url: '/bot/v1/teams', headers: bk(), payload: { human, title: '数据分析', leadBotId: ids['组长'], workerBotIds: [ids['数据员']] } })).json() as TeamDetailDto

+ 22 - 2
docs/design/13-bot-teams.md

@@ -1,6 +1,6 @@
 # 13 Bot 工作组
 
-> 状态:**一期(2026-10-26)、二期与三期(2026-10-27)已实施**;实施中的调整见 §15–§18。决策见 ADR-0023(工作组协作模型)、ADR-0024(Bot API 与独立的 Bot Key)。
+> 状态:**一期(2026-10-26)、二期与三期(2026-10-27)已实施**;实施中的调整见 §15–§19。决策见 ADR-0023(工作组协作模型)、ADR-0024(Bot API 与独立的 Bot Key)。
 
 ## 1. 背景与目标
 
@@ -357,7 +357,7 @@ SSE 事件:
 |---|---|---|
 | T1 | 组长的提示词和协作规范放在哪里 | ✅ 内置"组长"Profile(每个引擎一个)和 team-lead 技能,后台可以修改或复制(§18) |
 | T2 | 组长能否申请移除组员 | 一期、二期不支持,由成员操作 |
-| T3 | 工作组长期运行后,组长的 Session 越来越长 | 任务纪要已实施(§18);何时为组长重开 Session 待定 |
+| T3 | 工作组长期运行后,组长的 Session 越来越长 | ✅ 任务纪要(§18)+ 按上下文大小和闲置时间开始新会话(§19) |
 | T4 | 能力卡片里是否列出全部 MCP 工具 | 只列服务名和说明,工具较多时不展开 |
 
 ## 15. 一期实施记录(2026-10-26)
@@ -445,3 +445,23 @@ SSE 事件:
 - 任务结束(完成、失败、取消)时,系统把任务的计划、每个子任务的说明、结果和文件、交付内容,以及组长在 `job_complete` / `job_fail` 的 `notes` 中写的要点,保存到共享目录的 `.team/jobs/job-<编号>.md`。
 - 纪要由系统根据任务记录生成,不需要组长额外执行一轮;组长只需写几行"给以后的自己"的要点。
 - 组长的提示词中列出最近 5 份纪要的路径和标题,遇到相关工作时可以读取。
+
+## 19. 开始新会话与文件归属(2026-10-27)
+
+**开始新会话**(T3 的第二步):
+- 机器人每一轮开始前判断是否换新会话,满足任一条件就换:
+  - 上一轮最后一次模型调用的上下文超过 `limits.sessionMaxContextTokens`(默认 100k token)
+  - 闲置超过 `limits.sessionIdleHours`(默认 24 小时;提示词缓存早已过期,续接长会话每轮都要按全价重读历史)
+  - 成员要求(`POST /teams/:id/bots/:botId/reset-session`,Playground 成员页签的"新会话")
+- 组长只在两项任务之间换:任务进行中的来龙去脉在会话里,换了反而丢失。组员每一轮都可以换,因为子任务的输入是完整的。
+- 换会话时工作组中出现一条系统消息。新会话靠两样东西接上之前的工作:组长提示词中的最近任务纪要,以及 `team_history`。
+- 不按"每 N 个任务"换:这和任务长短无关,会让短任务也频繁丢掉上下文。
+- 上下文大小:Claude 取主线程最后一条 assistant 消息的 usage,Codex 取 `tokenUsage.last`,都是输入 + 输出;记录在 `runs.context_tokens`。
+- 两个阈值在"设置 › 工作组限额"中,工作组可以调低。
+
+**文件归属**(一期记录中的遗留问题):一轮结束时,共享目录中改动的文件按以下规则算作这个机器人的产出:
+1. 声明的文件(`task_complete` / `job_complete` 的 files)为准。
+2. 写文件工具的记录:Claude 的 Write / Edit / MultiEdit / NotebookEdit、Codex 的文件修改、`recalc_workbook`(写回输入文件)、`convert_document`(结果中的"已生成"路径)。
+3. 只能靠扫描发现的文件(命令、脚本生成的):只有这一轮没有其他机器人同时工作时才算给它;有并发时不挂到回复上,仍然可以在"文件"页签中看到。
+- 组员的提示词要求把命令或脚本生成的交付文件列在 files 中。
+- 没有采用"每个机器人写到自己的 outputs/<名字>/":交付路径变复杂,组长写说明时也要遵守这条规则。

+ 5 - 3
docs/integration.md

@@ -47,9 +47,9 @@ app.post('/api/agent-token', requireLogin, async (req, res) => {
 SDK 由 AgentServer 自己提供,三个包需要一起安装:
 
 ```bash
-pnpm add https://agent.zenpower.cc/sdk/agent-server-shared-0.3.0.tgz \
-         https://agent.zenpower.cc/sdk/agent-server-client-0.3.0.tgz \
-         https://agent.zenpower.cc/sdk/agent-server-chat-ui-0.3.0.tgz
+pnpm add https://agent.zenpower.cc/sdk/agent-server-shared-0.3.1.tgz \
+         https://agent.zenpower.cc/sdk/agent-server-client-0.3.1.tgz \
+         https://agent.zenpower.cc/sdk/agent-server-chat-ui-0.3.1.tgz
 ```
 
 | 包 | 内容 |
@@ -224,9 +224,11 @@ await bots.teams.pause(id) / resume(id) / close(id)
 await bots.teams.addBot(id, botId) / removeBot(id, botId)
 await bots.teams.jobs(id)                                 // 任务与子任务
 await bots.teams.usage(id)                                // 按机器人、任务、子任务统计的用量
+await bots.teams.resetSession(id, botId)                  // 让机器人开始新的会话(组长在当前任务结束后)
 ```
 
 说明:
 - 一个工作组只有一个成员(`human`),平台用户之间不共享工作组。
 - 限额(子任务数、组长轮数、时长、成本)以 AgentServer 的系统设置为上限,创建工作组时只能调低。
+- 机器人的会话上下文过长或闲置太久时会自动开始新的会话(组长在两项任务之间换),之前的工作从共享目录 `.team/jobs/` 下的任务纪要了解。
 - 替身组员、逐步执行是后台 Bot Playground 的调试功能,平台不能设置。

+ 3 - 3
examples/platform-demo/package.json

@@ -9,9 +9,9 @@
     "typecheck": "vue-tsc --noEmit"
   },
   "dependencies": {
-    "@agent-server/chat-ui": "AGENT_SERVER_URL/sdk/agent-server-chat-ui-0.3.0.tgz",
-    "@agent-server/client": "AGENT_SERVER_URL/sdk/agent-server-client-0.3.0.tgz",
-    "@agent-server/shared": "AGENT_SERVER_URL/sdk/agent-server-shared-0.3.0.tgz",
+    "@agent-server/chat-ui": "AGENT_SERVER_URL/sdk/agent-server-chat-ui-0.3.1.tgz",
+    "@agent-server/client": "AGENT_SERVER_URL/sdk/agent-server-client-0.3.1.tgz",
+    "@agent-server/shared": "AGENT_SERVER_URL/sdk/agent-server-shared-0.3.1.tgz",
     "vue": "^3.5.44"
   },
   "devDependencies": {

+ 1 - 1
packages/chat-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@agent-server/chat-ui",
-  "version": "0.3.0",
+  "version": "0.3.1",
   "private": true,
   "type": "module",
   "exports": {

+ 1 - 1
packages/client/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@agent-server/client",
-  "version": "0.3.0",
+  "version": "0.3.1",
   "private": true,
   "type": "module",
   "exports": {

+ 2 - 0
packages/client/src/bot.ts

@@ -38,6 +38,8 @@ export class BotClient extends HttpClient {
     addBot: (id: string, botId: string) => this.request<TeamDetailDto>('POST', `/bot/v1/teams/${id}/bots`, { botId }),
     removeBot: (id: string, botId: string) => this.request<TeamDetailDto>('DELETE', `/bot/v1/teams/${id}/bots/${botId}`),
     interruptBot: (id: string, botId: string) => this.request<{ ok: true }>('POST', `/bot/v1/teams/${id}/bots/${botId}/interrupt`),
+    /** 让机器人开始新的会话(组长在当前任务结束后) */
+    resetSession: (id: string, botId: string) => this.request<{ ok: true }>('POST', `/bot/v1/teams/${id}/bots/${botId}/reset-session`, {}),
     send: (id: string, body: SendTeamMessageBody) => this.request<TeamMessage>('POST', `/bot/v1/teams/${id}/messages`, body),
     messages: (id: string, after = 0) => this.request<{ items: TeamEvent[]; lastSeq: number }>('GET', `/bot/v1/teams/${id}/messages?after=${after}`),
     events: (id: string, after = 0) => this.request<{ items: TeamEvent[]; lastSeq: number }>('GET', `/bot/v1/teams/${id}/events?after=${after}`),

+ 1 - 1
packages/shared/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@agent-server/shared",
-  "version": "0.3.0",
+  "version": "0.3.1",
   "private": true,
   "type": "module",
   "exports": {

+ 2 - 0
packages/shared/src/events.ts

@@ -52,6 +52,8 @@ export interface UsagePayload {
   cacheReadTokens: number
   cacheWriteTokens: number
   costUsd?: number
+  /** 本轮最后一次模型调用的上下文大小(输入 + 输出),用于判断会话是否过长 */
+  contextTokens?: number
 }
 
 export interface EventPayloads {

+ 6 - 1
packages/shared/src/teams.ts

@@ -82,9 +82,14 @@ export interface TeamLimits {
   maxMinutesPerJob: number
   /** 工作组同时执行的 Run 数 */
   maxConcurrentRuns: number
+  /** 机器人会话的上下文超过这个 token 数时,下一轮开始新的会话(组长在两项任务之间);null 表示不限 */
+  sessionMaxContextTokens: number | null
+  /** 机器人闲置超过这个小时数后,下一轮开始新的会话;null 表示不限 */
+  sessionIdleHours: number | null
 }
 
-export type TeamLimitKey = Exclude<keyof TeamLimits, 'maxConcurrentRuns'>
+/** 任务级的限额(超出时任务暂停) */
+export type TeamLimitKey = Exclude<keyof TeamLimits, 'maxConcurrentRuns' | 'sessionMaxContextTokens' | 'sessionIdleHours'>
 
 /** 调试设置:只有来源为 admin 的工作组(Bot Playground)可以设置 */
 export interface TeamDebug {